Unit III — Parallel Algorithms & Design

Designing Parallel Algorithms

Session 07 • 2311CSC501J — Parallel Processing

What You'll Learn

  • Foster's PCAM design methodology
  • Data vs functional vs recursive decomposition
  • Granularity: fine vs coarse tasks
  • Task graphs & the critical path (the ceiling)

"Parallelism is a design decision, not a finishing touch. Decide where the work splits before you write a line."

— The whole session in one line

Design for It — Don't Retrofit

The classic mistake: write the whole program sequentially, then try to "sprinkle threads on top" at the end. It almost always produces a slow, buggy mess — cores fighting over data, work unevenly split, half the machine idle.

A good architect plans the plumbing and load-bearing walls first. In a parallel program, three things are load-bearing — decide them early:

Where work splits

Into what independent pieces?

What data moves

Which piece needs which other's result?

What depends on what

What's stuck in sequence, forever?

Those three decisions are exactly Foster's PCAM — a four-step method to design a parallel algorithm for any problem.

Foster's PCAM Methodology

Stage Name The question it answers
PPartitionBreak the problem into the smallest independent tasks
CCommunicationWhich tasks need data from which other tasks?
AAgglomerationGroup tasks to cut communication & overhead
MMappingWhich processor/core runs each group?

P + C — Expose parallelism

Think small, think independent. Ignore the hardware for now. Aim for 10× more tasks than processors.

A + M — Make it practical

Group and place that work so it runs fast on real hardware.

Expand, then contract. Design in that order and you rarely go wrong.

PCAM in Action: A Weather Grid

A 1000×1000 grid of temperatures. Each step, every cell updates from its 4 neighbors. At the end we want the average. How do we parallelize it?

Partition → one task per cell = a million tiny tasks (domain decomposition — same rule, different cells).

Communication → each cell needs its N/S/E/W neighbors (local, cheap). The final average sums all cells (global, costly).

Agglomeration → group cells into 100×100 tiles. Neighbor chatter becomes memory reads; only tile edges still talk.

Mapping → give each core an equal band of tiles; keep neighboring tiles on nearby cores (network locality — Session 04).

P & C expose the parallelism; A & M make it practical. Same four steps decompose a monolith into microservices.

Three Ways to Split a Problem

Data / Domain

Same operation, different data slices.

Sum an array • resize 1000 images. Most common; scales beautifully. GPUs live here.

Functional / Task

Different operations, often a pipeline.

Video: decode → filter → encode. Limited by how many distinct stages exist.

Recursive

Divide & conquer — same-shaped sub-problems.

Quicksort, merge sort, tree reduction. Naturally spawns independent sub-tasks.

Also worth naming: Exploratory (search a space; first to find a solution stops the rest) and Speculative (start work you might need; discard the waste). Real systems mix all of these.

Granularity: How Big Is a Task?

Fine-grained

Many small tasks.

+ Great load balance

– High overhead (scheduling & comms)

Too fine → you parallelize yourself slower.

Coarse-grained

Few big tasks.

+ Low overhead

– Load imbalance risk (idle cores)

Too coarse → 7 cores wait for the 8th.

Think computation-to-communication ratio: you want the useful work to dwarf the coordination tax. The art is landing in the middle — big enough that overhead is negligible, small enough that work spreads evenly.

Worked Example: How Big Should a Task Be?

1,000,000 items. Each takes 1 µs of real work — so 1 second serially. Creating and scheduling a task costs 10 µs. You have 8 cores. The only decision: how many items go in one task?

Items per task Tasks Total overhead Wall-clock on 8 cores Speedup
1 (too fine)1,000,00010 s1.375 s0.7× — slower than serial
10010,0000.10 s0.138 s7.3×
1,000 (the sweet spot)1,0000.01 s0.126 s7.9×
125,0008 (one each)0.00008 s0.125 s8.0× — but see below
1,000,000 (too coarse)10.00001 s1.0 s1.0× — seven cores idle
speedup
   8x |            ____________
      |          /             \
      |         /               \        <-- fall off BOTH edges
      |        /                  \
   1x |_______/                     \____
      +-------------------------------------
       1      100   1,000   125,000   1,000,000     items per task
       ^                                     ^
    overhead eats it              no parallelism left

Row 1 is the classic beginner disaster: "I made it maximally parallel — one task per item!" — and the program is now slower than the serial version it replaced. The parallelism was real. The bookkeeping cost ten times more than the work.

Row 4 is the trap that looks like the answer. Exactly 8 tasks for 8 cores gets the best number on paper — and has zero slack. If one task happens to be twice as slow (a bigger image, a colder cache, a busy core), seven cores sit idle waiting for it and your 8× becomes 4×. There is no work left to rebalance with.

The rule of thumb professionals actually use: aim for roughly 10× as many tasks as cores, and make each task at least ~100× the scheduling overhead. That's enough tasks for the runtime to smooth out imbalance, and each is big enough that the bookkeeping disappears.

Granularity is the one PCAM decision you tune by measuring, not reasoning. The curve above is real — find its top for your own problem.

Dependencies & Task Graphs

Every task = a node. Arrow A→B means B needs A's result (B can't start until A finishes). No cycles allowed → a Directed Acyclic Graph (DAG).

Sum 8 numbers as a reduction tree

  a   b   c   d   e   f   g   h     <- 8 inputs (level 0)
   \ /     \ /     \ /     \ /
   ab      cd      ef      gh       <- 4 adds, all independent (run at once)
     \    /          \    /
     abcd            efgh           <- 2 adds, independent
        \            /
          abcdefgh                  <- 1 final add = the answer

Tasks with no arrow between them are independent — they can run in parallel. The four bottom adds have no dependency on each other, so 4 cores do them at once. The graph literally shows you your parallelism.

The Critical Path Is Your Ceiling

No matter how many processors you have, you can never finish faster than the critical path.

The critical path is the longest chain of dependent tasks from start to finish. In the 8-number tree:

7
adds in sequence (1 core)
3
levels = critical path (∞ cores)
2.3×
best possible speedup (7/3)

We found the ceiling before running anything. This is the graph-shaped preview of Amdahl's Law — the "nine musicians, thirty seconds" of Session 01, made precise. Speeding up a task off the critical path changes nothing.

Worked Example: Find the Critical Path

A CI pipeline. Eight stages, real dependencies, times in minutes.

                          B: compile mod1 (10)
                        /                      \
  A: fetch src (2) ---- C: compile mod2  (4) ---- E: link (3)
                        \                      /            |
                          D: compile mod3  (6)              |
                                                            |
                                          +-----------------+-----------------+
                                          |                                   |
                                    F: unit tests (8)          G: integration tests (12)
                                          |                                   |
                                          +-----------------+-----------------+
                                                            |
                                                     H: package (1)

Total work (T₁)

Add every box:
2+10+4+6+3+8+12+1

46 min

What one machine takes.

Critical path (T∞)

Longest chain:
A → B → E → G → H
2+10+3+12+1

28 min

What infinite machines take.

Maximum speedup = 46 / 28 = 1.64×. That's it. Not 8×, not 100×. This pipeline can never finish faster than 28 minutes no matter how many machines you rent, because those five stages must happen one after another.

Machines Finish time Why
146 minEverything in sequence
230 minSome compiles and tests overlap
328 minAll three compiles run at once — the ceiling is reached
10028 min97 machines idle. Zero benefit.

Now the lesson that saves careers. Suppose you spend a week optimising C: compile mod2 from 4 minutes down to 1. New pipeline time?

Still 28 minutes. Exactly zero improvement.

C is not on the critical path. Time spent optimising anything off the critical path is completely wasted — and it is the single most common way engineering effort is thrown away.

Do this instead: attack G, the 12-minute integration tests — the biggest box on the critical path. Halve it to 6 and the pipeline drops to 24 minutes (the path through F now binds). Then re-find the critical path, because it moves. That loop — find it, shorten it, find it again — is how real performance work is done.

Work, Span & Maximum Speedup

Work — T₁

Total number of tasks.

= time on one processor (do them all in sequence).

Span — T∞

Critical-path length.

= time on infinite processors.

                             Work        T1
   Maximum speedup  =  ----------  =  ------
                             Span        T-inf

Everything else in this course is about getting close to that ceiling on real hardware — finite cores, real communication cost, load imbalance. For the 8-sum: T₁ = 7, T∞ = 3, ceiling ≈ 2.3×.

Watch: Granularity & the Critical Path

Comrevo — 18:38 total. Show ~8:00 to ~15:00, the task-graph and critical-path part. The opening repeats slides 5–7.

Someone else drawing the same graph and tracing the same longest path. The video says degree of concurrency — that is just how many tasks can run at once at a given moment, and it is not examinable here. Work over span is the number that matters.

Worked Example: Two Numbers Predict Everything

Same pipeline. Work T₁ = 46. Span T∞ = 28. From those two numbers alone you can predict the runtime on any machine, before writing a line of code.

You can never beat:      T_p  >=  max( T1/p , T_inf )
You are guaranteed:      T_p  <=  T1/p  +  T_inf         (Brent's bound)

Parallelism  =  T1 / T_inf  =  46 / 28  =  1.64
             =  the most processors that can ever be useful
Processors T₁/p T∞ Which one binds? Best speedup
14628work — not enough processors1.0×
22328span — already!1.64×
85.828span1.64×
1,0000.0528span1.64×

From two processors onwards, the span is in charge and nothing else matters. Buying processor number 3 through 1,000 changes precisely nothing. Two numbers, computed on paper in five minutes, told you that.

And here is the connection that ties this session to the next one. The span is just the sequential part of your program, drawn as a graph. So:

sequential fraction  f  =  T_inf / T1  =  28 / 46  =  0.61
max speedup             =  1 / f       =  1 / 0.61 =  1.64   <-- the same answer

That is Amdahl's Law, which we make formal in Session 08. Work-and-span and Amdahl are not two ideas — they're the same statement, one drawn as a graph and one written as a fraction.

What to actually do with this: before you parallelise anything, sketch the task graph and compute T₁/T∞. If the answer is 1.6, do not buy a cluster — go and restructure the algorithm to shorten the span. If it's 500, you have a genuinely parallel problem and you can go shopping for cores.

This Is How Real Systems Are Built

Design concept The same idea at company scale
PartitionBreak a monolith into microservices; shard a database
CommunicationThe API calls & message queues between those services
AgglomerationTwo services chat constantly → merge them; batch small jobs
MappingWhich servers/pods run which workload, in which region
Critical pathThe longest dependency chain in any project plan (Gantt chart)

Foster wrote PCAM for supercomputers in 1995. It describes a modern cloud architecture diagram just as well. Thread → core → GPU → server → data center is one continuous idea.

The Design Checklist

Face a new problem? Run this — PCAM plus the ceiling check — before you write code:

Do this up front and you avoid the "sprinkle threads on at the end" disaster. Parallelism becomes four clear questions you already know how to answer.

Your Turn: Design It, Don't Code It

In pairs, 8 minutes. The job: 50,000 property photos. For each one — download it (2 s, network), detect rooms with a model (0.5 s, GPU), generate three thumbnails (0.1 s, CPU), write to storage (1 s, network). Then produce one summary report over all 50,000.

P — Partition: what is one task?

One photo. The photos are fully independent, so this is a data decomposition and it's embarrassingly parallel — right up until the report.

C — Communicate: who talks to whom?

Nobody, during the work. Only at the end, when all 50,000 results must be combined into one report. That single join is your entire span.

A — Agglomerate: what's the task size?

Not one photo per task — batch ~50 photos. Enough to amortise startup, and 1,000 batches for 8–32 workers gives plenty of slack to rebalance.

M — Map: what runs where?

Download and upload are I/O-bound → many concurrent connections, few cores. Detection is GPU. Thumbnails are CPU. Three different resources, three different worker counts.

Now the arithmetic that decides the design. Per photo: 3 s of waiting on the network, 0.6 s of computing. That's 83% I/O.

So the naive "one thread per photo, do all four steps" design leaves your GPU idle 83% of the time. The right shape is a pipeline: downloads streaming in continuously, feeding a GPU that never stops, feeding uploads going continuously out. Overlap the waiting with the computing — which is Session 01's definition of concurrency, arriving as a design decision.

Sanity-check with work and span: T₁ ≈ 50,000 × 3.6 s = 50 hours. T∞ ≈ one photo's chain (3.6 s) plus the final report. Parallelism is in the tens of thousands — so this problem genuinely scales, and the only real questions are cost and how fast your storage is.

Compare that with the CI pipeline, where the same analysis said "1.64×, don't bother." Same four questions, opposite conclusions, both reached before writing any code. That is what PCAM is for.

Recap & What's Next

Key Takeaways

Homework

Next session: Measuring Parallel Performance

Amdahl's & Gustafson's Laws — today's "critical path is the ceiling," turned into hard numbers.