Parallelism & Multicore
Chapter: 6.1 Parallelism & Multicore Difficulty: Level 5 — Mastery (cross-domain: math + architecture + coding) Time limit: 90 minutes Total marks: 60
Instructions: Answer all three questions. Show all derivations. Code may be written in C-like pseudocode or C11. State every assumption. Marks are shown per part.
Question 1 — Scaling laws, coherence traffic, and the real speedup ceiling (24 marks)
A dense linear-algebra kernel runs on a shared-memory NUMA machine. Profiling on 1 core shows:
- Serial (non-parallelizable) fraction of the original work: .
- The parallel region performs floating-point operations.
- Each core sustains FLOP/s in the parallel region.
(a) Using Amdahl's Law, derive the general speedup and compute the maximum achievable speedup as . Then compute . (5)
(b) The team argues Amdahl is too pessimistic because they will grow the problem. Under Gustafson's Law with the same serial fraction (measured on the parallel run), derive the scaled speedup and evaluate . Explain in one sentence the modelling assumption that makes . (5)
(c) Reality intrudes: every core, once per parallel iteration, updates a shared counter guarded by a cache line that must be transferred between cores. On this directory-based coherence system a remote cache-line acquisition costs , and there are such synchronized iterations. Model the total runtime on cores as
where FLOP/s, contributes a fixed serial time with , and the last term models coherence-traffic contention growing linearly in . Find the value of that minimizes (treat as continuous, then round), and state the minimum runtime. (9)
(d) The shared counter causes false sharing in an unrelated per-core statistics array laid out adjacently. Explain the mechanism, and give a concrete code-level fix (padding/alignment) with the cache-line size assumed. State how your fix changes the term qualitatively. (5)
Question 2 — Correct lock-free synchronization with CAS (20 marks)
You must implement a lock-free bounded increment: multiple threads increment a shared 64-bit counter but it must never exceed a cap CAP.
(a) Write a bool try_bounded_inc(atomic_uint64 *ctr, uint64 CAP) using compare-and-swap (CAS(addr, expected, desired) returns true on success). It must return false when at the cap, and be correct under contention. (7)
(b) Prove your loop is lock-free (system-wide progress) but not necessarily wait-free (per-thread bound). Give a concrete interleaving of 2 threads where one thread's CAS fails an unbounded number of times in principle. (6)
(c) Explain the ABA problem in the general CAS setting and argue precisely why the bounded-increment counter above is immune to ABA, even though it uses raw CAS. (4)
(d) Contrast this fine-grained atomic approach with a barrier-based bulk-synchronous alternative for the same aggregate work. State one workload characteristic that favors each. (3)
Question 3 — SIMD vectorization and heterogeneous offload (16 marks)
Consider the SAXPY operation for , single precision, .
(a) Using AVX (256-bit registers, 8 fp32 lanes) with a fused multiply-add, compute the number of vector iterations, and the ideal arithmetic speedup vs scalar assuming one FMA per lane per cycle in both cases. State which of Flynn's classes AVX belongs to and why. (5)
(b) The array length is (not a multiple of 8). Write pseudocode showing the vectorized main loop plus scalar remainder (tail) loop, and state exactly how many vector iterations and scalar iterations run. (5)
(c) You offload SAXPY to a manycore GPU (heterogeneous computing). The data transfer over PCIe costs each way, and the GPU computes the kernel in ; the CPU (AVX) does it in . Compute the effective GPU time including transfers and decide whether offload is worthwhile for this single call. Then state the condition (in terms of number of back-to-back calls reusing resident data) under which offload wins. (6)
Answer keyMark scheme & solutions
Question 1
(a) Amdahl's Law: with serial fraction , parallel fraction perfectly split over : (2 marks derivation)
As : . (1 mark)
. (2 marks)
(b) Gustafson: total scaled work per processor is , ideal serial time , parallel time : (3 marks)
. (1 mark)
Modelling assumption: Gustafson fixes time and lets problem size grow with , so the serial part stays constant while parallel work scales up — the serial fraction shrinks relative to total work, avoiding Amdahl's fixed ceiling. (1 mark)
(c) With , :
- Parallel time term: s.
- s (constant).
- Coherence: s.
(3 marks setup)
Differentiate, ignoring constant terms: (3 marks)
Round to (check neighbours). s. s; slightly lower. Optimum , s (≈69.6 ms). (3 marks; accept p=30 or 31 with justification)
(d) False sharing mechanism (2): the per-core statistics array elements sit in the same cache line as the shared counter (or adjacent entries share a line). Even though cores write logically-independent variables, the coherence protocol invalidates the whole line on every write, forcing ping-pong transfers — no true data dependency, pure layout artifact.
Fix (2): pad/align each per-core entry to a full cache line (assume 64 bytes):
struct alignas(64) PerCore { uint64_t stat; char pad[64-sizeof(uint64_t)]; };
PerCore stats[NCORES]; // each in its own lineEffect (1): eliminating false sharing removes the spurious invalidations, so the contention term shrinks toward only the genuinely shared counter's traffic — it does not vanish (the real counter still bounces) but the multiplier on coherence misses drops.
Question 2
(a) (7 marks: correct CAS loop, cap check, return values, atomicity)
bool try_bounded_inc(atomic_uint64 *ctr, uint64 CAP) {
uint64 cur = atomic_load(ctr);
for (;;) {
if (cur >= CAP) return false; // at cap, refuse
uint64 next = cur + 1;
if (CAS(ctr, &cur, next)) return true; // success
// CAS failed: 'cur' is refreshed to actual value, retry
}
}Key points: reload-on-fail (CAS updates cur), cap re-checked each iteration, no lock.
(b) (6 marks) Lock-free: whenever any thread's CAS fails, it failed because another thread's CAS succeeded — so the system as a whole made progress (the counter advanced). Hence at least one thread always completes; no global stall. (3)
Not wait-free: interleaving — Thread A loads cur=5, then before A's CAS, Thread B successfully increments to 6, then Thread C to 7, ... A's CAS keeps failing. In principle other threads can succeed every time just before A, so A retries unboundedly; there is no per-thread step bound. (3)
(c) (4 marks) ABA: a location changes A→B→A between a thread's read and its CAS; the CAS wrongly succeeds because it only sees value equality, missing the intermediate change — dangerous when the value is a pointer/tag whose meaning changed. (2) Immunity here: the counter is monotonically non-decreasing up to CAP; it never revisits a prior value, so no A→B→A sequence can occur. Value equality therefore does imply "unchanged since read." Raw CAS is safe. (2)
(d) (3 marks) Fine-grained CAS: low latency, good when increments are sparse/irregular and threads proceed independently. Barrier BSP: threads compute local partial sums, then one synchronized reduction — better when work is regular, bulk, and per-thread contention on the shared counter would otherwise dominate (avoids the ping-pong of (b)). Favor CAS for irregular/low-contention; favor barrier+reduction for dense/high-contention bulk workloads.
Question 3
(a) (5 marks) Vector iterations . (1) Ideal speedup with 8 lanes and one FMA/lane/cycle vs scalar one FMA/cycle: 8×. (2) AVX is SIMD (Single Instruction, Multiple Data): one instruction applies the same FMA op across 8 data lanes simultaneously. (2)
(b) (5 marks) For : vector iterations (covers indices 0..4095); scalar tail iterations. (2 for counts)
size_t i = 0;
size_t vend = n - (n % 8); // 4096
for (; i < vend; i += 8) { // 512 vector iters
__m256 vx = _mm256_loadu_ps(&x[i]);
__m256 vy = _mm256_loadu_ps(&y[i]);
vy = _mm256_fmadd_ps(_mm256_set1_ps(a), vx, vy);
_mm256_storeu_ps(&y[i], vy);
}
for (; i < n; ++i) // 4 scalar tail iters
y[i] = a*x[i] + y[i];(3 for correct main+tail structure)
(c) (6 marks) Effective single-call GPU time . (2) CPU AVX → offload NOT worthwhile for a single call. (2) For back-to-back calls reusing resident data (transfer once, or amortized): GPU ; CPU . Offload wins when , i.e. calls. (2)
[
{"claim":"Amdahl S(16) with s=0.08 equals 80/11 approx 7.2727","code":"s=Rational(8,100); SA=1/(s+(1-s)/16); result=(SA==Rational(80,11)) and abs(float(SA)-7.27272727)<1e-6"},
{"claim":"Gustafson S(16)=14.8","code":"s=Rational(8,100); SG=s+(1-s)*16; result=(SG==Rational(74,5)) and float(SG)==14.8"},
{"claim":"Optimal continuous p for T(p) is sqrt(0.46/4.9152e-4) approx 30.59","code":"popt=sqrt(Rational(46,100)/Rational(49152,100000000)); result=abs(float(popt)-30.594)<0.01"},
{"claim":"GPU offload breakeven: k>=3 calls (100+3k<40k)","code":"k=symbols('k'); sol=solve(100+3*k-40*k,k)[0]; result=(abs(float(sol)-Rational(100,37))<1e-9) and (3>float(sol)) and (2<float(sol))"},
{"claim":"AVX vector iters for n=4100 is 512 and scalar tail is 4","code":"n=4100; result=(n//8==512) and (n%8==4)"}
]