The Trapezoid,
Three Ways, in Threads
Week 4's problem again: x² on [0, 5]. The old course code solves it three times, and each version deletes something. Two of them also bring a bug back.
Concept map
Monday gave you the words. Today is one file that uses all of them. Click a node to jump to it.
Same problem. What changed?
f(x) = x², [0, 5], n = 1.024·10⁸ today (the Cardinal job uses Week 4's 1.024·10⁹). Exact answer 125/3 = 41.666…
| Week 4 · MPI | Today · OpenMP | |
|---|---|---|
| Who am I | MPI_Comm_rank, MPI_Comm_size | omp_get_thread_num, omp_get_num_threads |
| Getting a, b, n | rank 0 sets them; four MPI_Bcasts | already there: they are shared |
| My strip | myA = a + rank·(n/p)·h | the same line, with the thread id |
| The merge | MPI_Reduce(SUM) to rank 0 | critical (1, 2), then reduction (3) |
| The split bug | n/p kept a fraction; Trap took a long | n/t in int, and myA stored in an int |
| Who hid it | p = 1, 2, 4, 8 divide 1.024·10⁹ | the old test used [0, 16] with n = 16 |
thirdMethod broadcast a, b, h and n because rank 3 had no other way to see rank 0's variables. What does the OpenMP version do instead? Everything that moved data is gone. firstprivate would also work, but it is unnecessary: the values are only read. What did not go away is the arithmetic of the split — and neither did its bug.
TrapRule1: split by hand, critical inside
As written in 2018. main opens #pragma omp parallel and makes one call. Every thread runs the whole function.
void parallelTrap(double a, double b, int n, double& result){
double h, x, myRetValue;
int myId = omp_get_thread_num();
int numThreads = omp_get_num_threads();
int myN, myA, myB;
h = (b - a) / n;
myN = n / numThreads;
myA = a + myId * myN * h;
myB = myA + myN * h;
myRetValue = (f(myA) + f(myB)) / 2.0;
for (int i = 1; i < myN; i++) {
x = myA + i * h;
myRetValue += f(x);
}
myRetValue = myRetValue * h;
#pragma omp critical
result += myRetValue;
}
Walk it
myIdandnumThreadsreplace rank and size.- Locals declared inside a function called from the region: each thread has its own. Monday's "declared inside is private".
resultis main's variable, passed by reference: shared.critical: t additions, one at a time. t, not n — cheap.- Look at the highlighted line. Do not say anything yet.
myA and myB are x-coordinates stored in an int. At t = 2, thread 1 should start at 2.5. It starts at 2.
[0, 5], n = 1.024e8 exact 41.667 t=1 41.666666666671155 CHECK ok t=2 32.916666507978192 CHECK WRONG t=3 32.407406631586333 CHECK WRONG t=4 29.479166538494464 CHECK WRONG t=5 41.666666666666224 CHECK ok The old input: [0, 16], n = 16 serial 1368 t=1, 2, 4, 8 1368 CHECK ok t=3, 5 1127.5 CHECK WRONG
Two bugs, both hidden by the test the old code used. On [0, 16] with n = 16, h = 1: every coordinate was already whole. And t = 3 with n = 16 gives myN = 5, 3 × 5 = 15 — strip [15, 16] is never added. That is Week 4's remainder bug, in integer form. Right at t = 5 on [0, 5] because 5/5 = 1: whole-number strip edges again.
The bug, explored
The same arithmetic TrapRule1 does, computed here. Move t and watch where each thread thinks its strip starts.
Edges that truncate are shown in red. The number is computed from the same formula the code uses — a closed form of the loop, so it runs instantly at n = 10⁸. It reproduces the slide's output to the printed digits.
TrapRule2: the caller merges
The worker now returns its strip. It no longer touches result, so it can be tested alone. Main owns the merge.
// parallelTrap() now RETURNS its strip (same int split) #pragma omp parallel num_threads(numThreads) { //Incorrect First option // #pragma omp critical // result += parallelTrap(a, b, n); double localResult; localResult += parallelTrap(a, b, n); #pragma omp critical result += localResult; }
Also note num_threads(numThreads) with numThreads = 4 hard-coded in main: Monday's backup bug. The port drops it so a sweep over t can work.
localResult before the +=? 2. The commented-out "Incorrect First option" puts the critical around the call. Is its answer right or wrong?Question 1 is Monday's private-vs-firstprivate trap in a new place: declared, never set, then added to.
$ make all (g++ 13 -O2) warning: 'localResult' is used uninitialized t=1..7 same digits as the TrapRule1 port (it happened to be 0) $ clang++ -O2 (same source, [0,16], n=16) t=1 trap2 nan t=2 trap2 nan t=4 trap2 nan trap2_CriticalCall t=1..7 CHECK ok
Under g++ it was 0 for years and nobody noticed. Under clang, nan at every t. The compiler owes you nothing, and it warned. Fix: double local = parallelTrap(…); — declare and set in one line. Question 2 is right, and that is the whole point of the next screen.
What the critical holds
A critical section is correct for any block of code. The question is only what you put inside it.
A model, not a timing: each strip is n/t trapezoids of work; the critical is drawn as a queue. Measured on Cardinal, Oct 19: trap2_CriticalCall ran 831 ms at t = 1 and 829 ms at t = 48 — speedup 1.00 at every t — while trap1 went from 834 ms to 17.8 ms.
The fix is Week 4's fix
fourthMethod with thread ids. Counts in long, coordinates in double, the first n % t threads take one extra.
// myStrip(): used by trap1, trap2, trap2_Reduction, trap2_CriticalCall int tid = omp_get_thread_num(); int t = omp_get_num_threads(); double h = (b - a) / n; long base = n / t; long extra = n % t; long myN = base; long first = tid * base + extra; if (tid < extra) { // the first n % t threads take one more myN = base + 1; first = tid * base + tid; } double myA = a + first * h; double myB = a + (first + myN) * h; // … the serial trapezoid over [myA, myB] with myN steps, times h …
// trap2: TrapRule2's shape, fixed #pragma omp parallel { double local = myStrip(a, b, n); // declared AND set #pragma omp critical result += local; // t updates, not n }
// trap2_Reduction: the merge as a clause #pragma omp parallel reduction(+:result) result += myStrip(a, b, n); // reduction works on a bare region too: // a private result per thread, added at the join
Explore the fixed split
Every trapezoid owned by exactly one thread; every edge a true double. CHECK ok at t = 1, 2, 3, 4, 7 on the test machine, and at 1 … 48 on Cardinal.
TrapRule3: the serial loop, plus one line
Put this next to TrapRule1. Everything the programmer computed by hand is now the runtime's job.
double h = (b - a) / n; double retValue = (f(a) + f(b)) / 2.0; #pragma omp parallel for reduction(+:retValue) for (long i = 1; i <= n - 1; i++) { retValue += f(a + i * h); } retValue = h * retValue; // The 2019 file still calls this serialTrap(). // The name is the only thing left over.
Gone
myId,numThreadsmyN,myA,myB— and both bugs- the critical
Who decided the split?
The schedule: contiguous blocks, the remainder spread exactly as on Monday's for_Owners. In MPI you wrote the Scatter. Here one pragma made the same decision, correctly, and did not show you.
schedule(dynamic), Wednesday Oct 28.Right to 10⁻¹³, and different in the last digits
Every correct version, t = 1, 2, 3, 4, 7. All CHECK ok against 125/3.
trap3 trap1 (manual split) t=1 41.666666666671155 41.666666666671155 t=2 41.666666666671055 41.666666666671048 t=3 41.666666666667766 41.666666666668483 t=4 41.666666666669542 41.666666666669542 t=7 41.666666666668064 41.666666666668419 exact 41.666666666666667 trap3, ten runs at t=4: ...542 ×8 ...549 ×2 trap3, ten runs at t=7: ...064 ×9 ...071 ×1
Each thread rounds its own partial sum, and (a + b) + c ≠ a + (b + c) in floating point. The t partials are combined in arrival order — so the same t can give a different last digit on the next run. trap1's strips start at different i than trap3's blocks, so the sums are grouped differently too. == is the wrong test. R05 checks within 1e-9 relative of the exact answer.
See it in your browser
This runs the actual sum here, in double precision: n = 10⁶ terms of f(a + i·h) split into t contiguous blocks, each block summed serially, then the t partials combined in the order shown. Same numbers, different grouping.
Run it on Cardinal
Same branch as Monday: lecture09. If you built it then, skip to the experiments. About 20 minutes.
-
Clone once, check out the lecture branch
ssh <your-username>@cardinal.osc.edu git clone -o class git@gitlab.com:cs4540-spring-2019/cs-4170-5170-fall-2026/openmp.git openmp cd openmp git fetch class git checkout -b lecture09 class/fa2026-openmp/lecture09
-
Build, then get cores of your own
One warning is expected (
'h' may be used uninitialized— Monday's bug). The login node is shared; anything you time goes in an allocation.module load gcc/12.3.0 cd Default && make OSC && cd .. salloc -N 1 -n 1 -c 8 -t 00:15:00 -A PCS0313
Every run:
OMP_NUM_THREADS=t srun -c 8 ./Default/OMP <method> [n [a b]]. The trapezoid methods:trap_Serial trap2_CriticalCall trap1 trap2 trap2_Reduction trap3. Default n = 1.024·10⁸.
Experiments
A · Every correct version, at an awkward t
for m in trap_Serial trap1 trap2 trap2_Reduction trap3; do
OMP_NUM_THREADS=3 srun -c 8 ./Default/OMP $m | grep -E '^trap|CHECK'
doneWhat you should see
Five CHECK … ok lines, and five numbers that agree to about 10⁻¹² and differ in the last three or four digits. Then run trap3 three more times at t = 3 and compare the final digits between runs.
B · What the critical holds: read ELAPSED
for t in 1 8; do
OMP_NUM_THREADS=$t srun -c 8 ./Default/OMP trap2_CriticalCall | grep -E 'CHECK|ELAPSED'
OMP_NUM_THREADS=$t srun -c 8 ./Default/OMP trap1 | grep -E 'CHECK|ELAPSED'
doneWhat you should see
All four ok. At t = 1 the two times are about the same. At t = 8, trap1 is roughly 8× faster than at t = 1 and trap2_CriticalCall is not faster at all. Same answer; the difference is what the critical holds.
C · Bring the bug back
The branch keeps only the fixed versions. Reintroduce TrapRule1's bug yourself: in src/main.cpp, inside myStrip, change double myA and double myB to int. Rebuild. Predict the number at t = 2 before you run it (the explorer on screen 4 will tell you if you were right).
cd Default && make OSC && cd ..
OMP_NUM_THREADS=2 srun -c 8 ./Default/OMP trap1
OMP_NUM_THREADS=5 srun -c 8 ./Default/OMP trap1
OMP_NUM_THREADS=3 srun -c 8 ./Default/OMP trap1 16 0 16
OMP_NUM_THREADS=4 srun -c 8 ./Default/OMP trap1 16 0 16
git checkout src/main.cpp && cd Default && make OSC && cd .. # put it backWhat you should see
t = 2: WRONG, about 32.9. t = 5: ok — 5/5 = 1, whole-number edges again. On the old input, t = 4 passes and t = 3 fails — which is exactly why it shipped in 2018. Then exit to release the allocation.
lecture09, run trap3 at t = 1 and 3, and look at the digits. R05 is today's trap3, timed — strong and weak scaling, t = 1 … 48 on one socket, with a tolerance check, never ==.Check yourself
Six questions, instant feedback, nothing recorded.
myA in an int. On [0, 5] with t = 2, where does thread 1's strip actually start, and where should it?#pragma omp critical around result += parallelTrap(a, b, n);. Right or wrong, and how fast?double localResult; localResult += parallelTrap(…); compiles with a warning and prints the right answer under g++. Is it correct?