Parallelism & Multicore
Level: 4 (Application — novel problems, no hints) Time limit: 60 minutes Total marks: 60
Question 1 — Scaling analysis (14 marks)
A video-encoding pipeline runs on a single core in ms. Profiling shows that ms is inherently serial (bitstream setup and I/O), and the remaining time is a perfectly parallelizable motion-search stage.
(a) Compute the parallel fraction of the workload. (2)
(b) Using Amdahl's Law, compute the speedup and the total runtime when the parallel stage runs on cores. (4)
(c) Compute the theoretical maximum speedup as core count . (2)
(d) The team wants an overall speedup of . Determine the minimum number of cores required (round up to an integer). (3)
(e) Now suppose the team scales the problem size so that the parallel stage grows to keep each of cores busy for the same ms as originally, while the serial part stays ms. Using Gustafson's Law, compute the scaled speedup and explain in one sentence why it differs from the Amdahl result in (b). (3)
Question 2 — Classification & architecture choice (12 marks)
For each scenario, name the correct Flynn taxonomy class (SISD/SIMD/MIMD) and justify in one line:
(a) A GPU warp of 32 threads all executing the same instruction on different pixels. (2) (b) An 8-node cluster each running an independent branch of a Monte-Carlo simulation with different code paths. (2) (c) A single-threaded pocket calculator evaluating one expression. (2)
(d) You must choose between a shared-memory system and a distributed-memory system for two workloads. State which is more appropriate and give one concrete reason for each: (i) A graph algorithm with fine-grained, irregular, frequent data sharing between all threads. (3) (ii) An embarrassingly parallel batch of 10 000 independent image resizes across 500 machines. (3)
Question 3 — False sharing & cache lines (12 marks)
Four threads each increment their own counter. The counters are declared as an array:
long counters[4]; // long = 8 bytes; cache line = 64 bytesThread repeatedly does counters[i]++ in a tight loop; there is no logical data sharing between threads.
(a) Explain why performance is nevertheless poor on a multicore system. Name the specific phenomenon. (3)
(b) How many of these long counters fit in one 64-byte cache line? Show whether all 4 counters share a line. (2)
(c) Propose a concrete code-level fix (padding/alignment) and state the new size in bytes of one padded counter struct so that each counter occupies its own cache line. (4)
(d) After the fix, a coherence protocol still generates traffic on genuinely shared data. Briefly contrast why directory-based coherence scales better than snooping-based coherence for a 128-core machine. (3)
Question 4 — Synchronization & atomics (12 marks)
Consider a lock-free stack push implemented with Compare-And-Swap (CAS):
do {
old_top = stack.top; // read
new_node.next = old_top;
// (some work)
} while ( !CAS(&stack.top, old_top, &new_node) );
(a) Explain what CAS does (its three operands and success condition) and why the loop is needed. (3)
(b) Under high contention with 16 threads pushing simultaneously, describe the failure/retry behaviour and its effect on scalability. (3)
(c) This design is vulnerable to the ABA problem. Give a concrete interleaving that causes incorrect behaviour, and name one standard mitigation. (4)
(d) Contrast a spinlock and a barrier: state the different purpose of each in one line apiece. (2)
Question 5 — SIMD throughput estimate (10 marks)
An AVX-512 unit processes 512-bit vector registers. A kernel adds two arrays of float (32-bit each), elements.
(a) How many float elements fit in one 512-bit register? (2)
(b) Assuming one vector add per cycle and ignoring load/store overhead, how many vector add instructions are needed to process all elements? (3)
(c) If the scalar version needs add instructions, what is the ideal instruction-count speedup from vectorization here? (2)
(d) Give two real-world reasons why the measured speedup is typically less than this ideal. (3)
Answer keyMark scheme & solutions
Question 1
(a) Parallel time ms. Parallel fraction Serial fraction . (2: 1 for parallel time, 1 for fraction)
(b) Amdahl speedup with : Runtime ms (equivalently ms). (4: 2 speedup formula/value, 2 runtime)
(c) As , : (2)
(d) Require : Minimum cores . (3: 1 setup, 1 algebra, 1 round-up/answer)
(e) Gustafson: scaled speedup using serial fraction of the scaled workload. Scaled total serial-equivalent time on 1 core ms; on 8 cores it takes ms. (Equivalently .) Reason: Amdahl fixes problem size (serial part dominates as cores grow), whereas Gustafson grows the problem with the machine, so the serial fraction shrinks and speedup scales nearly linearly. (3: 2 value, 1 explanation)
Question 2
(a) SIMD — one instruction stream applied across many data elements (pixels) in lockstep. (2)
(b) MIMD — independent instruction streams (different code paths) on different data across nodes. (2)
(c) SISD — single instruction stream, single data stream, one operation at a time. (2)
(d)(i) Shared memory — fine-grained, frequent irregular sharing benefits from a single address space with low-latency direct access; message passing would incur huge messaging overhead per share. (3)
(d)(ii) Distributed memory — independent tasks need no data sharing, so 500 machines scale out with no coherence/interconnect bottleneck; shared memory cannot span 500 machines cost-effectively. (3)
Question 3
(a) False sharing. All four counters lie in the same cache line; when one thread writes its counter, the coherence protocol invalidates that line in the other cores' caches even though they access different counters, forcing repeated cache-line ping-ponging. (3)
(b) longs per line. Since , all 4 counters fit in — and therefore share — a single 64-byte cache line. (2)
(c) Pad each counter to a full cache line, e.g.
struct alignas(64) PaddedCounter { long value; char pad[56]; };
PaddedCounter counters[4];Padded struct size bytes, so each counter occupies its own line and no false sharing occurs. (4: 2 padding idea/code, 2 size = 64)
(d) Snooping broadcasts every coherence transaction to all cores over a shared bus — bus bandwidth becomes the bottleneck at 128 cores ( traffic per event). Directory-based coherence keeps a directory recording which cores hold each line and sends point-to-point messages only to sharers, avoiding broadcast and scaling to large core counts. (3)
Question 4
(a) CAS(address, expected, new): atomically compares the value at address to expected; if equal it writes new and returns success, else it does nothing and returns failure. The loop retries because another thread may have changed stack.top between the read and the CAS; the CAS only commits if top is still old_top. (3)
(b) Under high contention many threads read the same old_top; only one CAS succeeds, the other 15 fail and re-loop. Wasted retries grow with thread count, so throughput plateaus and can degrade — the atomic memory location becomes a serialization hotspot. (3)
(c) ABA interleaving:
- T1 reads
old_top = A. - T1 is descheduled.
- T2 pops A, pops B, then pushes A back (top = A again), but A's
nextnow differs. - T1 resumes; CAS sees top still
== A, succeeds — but the stack structure changed and T1'snew_node.nextpoints to a stale/freed node, corrupting the stack. Mitigation: use a tagged/versioned pointer (add a monotonically increasing counter to the pointer so ABA is detected), or hazard pointers / double-width CAS. (4: 3 interleaving, 1 mitigation)
(d) Spinlock: provides mutual exclusion — one thread enters a critical section at a time (others busy-wait). Barrier: a synchronization point — all threads must arrive before any proceeds past it. (2)
Question 5
(a) floats per register. (2)
(b) vector add instructions. (3)
(c) Ideal speedup . (2)
(d) Any two: memory bandwidth / load-store overhead limits throughput (memory-bound kernel); data alignment and non-multiple-of-16 tail handling; SIMD width underutilization from branches; power/frequency throttling under AVX-512; loop overhead and dependencies. (3: 1.5 each)
[
{"claim":"Amdahl speedup on 8 cores = 3.902","code":"s=Rational(15,100); p=Rational(85,100); S=1/(s+p/8); result = abs(float(S)-3.90243902439)<1e-6"},
{"claim":"Max Amdahl speedup = 6.667","code":"s=Rational(15,100); result = abs(float(1/s)-6.6666666667)<1e-6"},
{"claim":"Cores needed for 5x speedup = 17","code":"s=Rational(15,100); p=Rational(85,100); N=symbols('N',positive=True); sol=solve(Eq(1/(s+p/N),5),N); import math; result = math.ceil(float(sol[0]))==17"},
{"claim":"Gustafson scaled speedup = 6.95","code":"s=Rational(15,100); p=Rational(85,100); result = abs(float(s+p*8)-6.95)<1e-9"},
{"claim":"AVX-512 vector adds needed = 65536 and speedup 16","code":"N=1048576; lanes=512//32; result = (N//lanes==65536) and (N//(N//lanes)==16)"}
]