Level 4 — ApplicationParallelism & Multicore

Parallelism & Multicore

60 minutes60 marksprintable — key stays hidden on paper

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 200200 ms. Profiling shows that 3030 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 pp of the workload. (2)

(b) Using Amdahl's Law, compute the speedup and the total runtime when the parallel stage runs on 88 cores. (4)

(c) Compute the theoretical maximum speedup as core count \to \infty. (2)

(d) The team wants an overall speedup of 5×5\times. 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 88 cores busy for the same 170170 ms as originally, while the serial part stays 3030 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 bytes

Thread ii 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), N=1,048,576N = 1{,}048{,}576 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 NN elements? (3)

(c) If the scalar version needs NN 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 =20030=170= 200 - 30 = 170 ms. Parallel fraction p=170200=0.85.p = \frac{170}{200} = 0.85. Serial fraction s=0.15s = 0.15. (2: 1 for parallel time, 1 for fraction)

(b) Amdahl speedup with N=8N=8: S=1s+p/N=10.15+0.85/8=10.15+0.10625=10.25625=3.902×.S = \frac{1}{s + p/N} = \frac{1}{0.15 + 0.85/8} = \frac{1}{0.15 + 0.10625} = \frac{1}{0.25625} = 3.902\times. Runtime =200/3.902=51.25= 200/3.902 = 51.25 ms (equivalently 30+170/8=30+21.25=51.2530 + 170/8 = 30 + 21.25 = 51.25 ms). (4: 2 speedup formula/value, 2 runtime)

(c) As NN\to\infty, p/N0p/N\to 0: Smax=1s=10.15=6.667×.S_{\max} = \frac{1}{s} = \frac{1}{0.15} = 6.667\times. (2)

(d) Require S=5S = 5: 5=10.15+0.85/N0.15+0.85N=0.20.85N=0.05N=17.5 = \frac{1}{0.15 + 0.85/N} \Rightarrow 0.15 + \frac{0.85}{N} = 0.2 \Rightarrow \frac{0.85}{N} = 0.05 \Rightarrow N = 17. Minimum cores =17= 17. (3: 1 setup, 1 algebra, 1 round-up/answer)

(e) Gustafson: scaled speedup S=s+pNS = s + p\cdot N using serial fraction of the scaled workload. Scaled total serial-equivalent time on 1 core =30+170×8=30+1360=1390= 30 + 170\times 8 = 30 + 1360 = 1390 ms; on 8 cores it takes 200200 ms. S=1390200=6.95×.S = \frac{1390}{200} = 6.95\times. (Equivalently s+pN=0.15+0.85×8=6.95s + pN = 0.15 + 0.85\times 8 = 6.95.) 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) 64/8=864 / 8 = 8 longs per line. Since 484 \le 8, 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 =8+56=64= 8 + 56 = 64 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 (O(N)O(N) 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 next now differs.
  • T1 resumes; CAS sees top still == A, succeeds — but the stack structure changed and T1's new_node.next points 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) 512/32=16512 / 32 = 16 floats per register. (2)

(b) N/16=1,048,576/16=65,536N / 16 = 1{,}048{,}576 / 16 = 65{,}536 vector add instructions. (3)

(c) Ideal speedup =N/(N/16)=16×= N / (N/16) = 16\times. (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)"}
]