|
| 1 | +""" |
| 2 | +proj2_analysis.py |
| 3 | +
|
| 4 | +Fill in the lists in the USER INPUT SECTION with your own timings. |
| 5 | +Run with: python proj2_analysis.py |
| 6 | +
|
| 7 | +Outputs: |
| 8 | +- Printed tables for OMP & MPI speedup and efficiency |
| 9 | +- Suggested "optimal" core/thread counts |
| 10 | +- Optional weak-scaling tables if you provide those data |
| 11 | +- Matplotlib plots that pop up and are also saved as PNG files |
| 12 | +""" |
| 13 | + |
| 14 | +import numpy as np |
| 15 | +import matplotlib.pyplot as plt |
| 16 | + |
| 17 | +# ===================== USER INPUT SECTION ===================== |
| 18 | + |
| 19 | +# Serial runtime (1 core, 1M-sample query) |
| 20 | +serial_time = 0.0 # <-- put your measured serial time in seconds here, e.g. 2.34 |
| 21 | + |
| 22 | +# OMP: list of #threads tested and corresponding runtimes (same 1M problem) |
| 23 | +omp_threads = [1, 2, 3, 4] # example thread counts |
| 24 | +omp_times = [0.0, 0.0, 0.0, 0.0] # <-- fill with your OMP runtimes in seconds |
| 25 | + |
| 26 | +# MPI: list of #processes tested and corresponding runtimes (same 1M problem) |
| 27 | +mpi_procs = [1, 2, 3, 4] # example process counts |
| 28 | +mpi_times = [0.0, 0.0, 0.0, 0.0] # <-- fill with your MPI runtimes in seconds |
| 29 | + |
| 30 | +# OPTIONAL (for part (c)): weak-scaling data, where problem size grows |
| 31 | +# proportionally with #threads/#processes. |
| 32 | +# Example: 1M, 2M, 3M, 4M samples when using 1, 2, 3, 4 threads. |
| 33 | +weak_problem_sizes_omp = [] # e.g. [1_000_000, 2_000_000, 3_000_000, 4_000_000] |
| 34 | +weak_times_omp = [] # your OMP runtimes for those sizes |
| 35 | + |
| 36 | +weak_problem_sizes_mpi = [] # e.g. [1_000_000, 2_000_000, 3_000_000, 4_000_000] |
| 37 | +weak_times_mpi = [] # your MPI runtimes for those sizes |
| 38 | + |
| 39 | +# =================== END USER INPUT SECTION =================== |
| 40 | + |
| 41 | + |
| 42 | +def compute_speedup_efficiency(serial_t, parallel_times, procs): |
| 43 | + parallel_times = np.array(parallel_times, dtype=float) |
| 44 | + procs = np.array(procs, dtype=float) |
| 45 | + speedup = serial_t / parallel_times |
| 46 | + efficiency = speedup / procs |
| 47 | + return speedup, efficiency |
| 48 | + |
| 49 | + |
| 50 | +def estimate_parallel_fraction(speedup, procs): |
| 51 | + """ |
| 52 | + Estimate parallel fraction f from Amdahl's law: |
| 53 | +
|
| 54 | + S(p) = 1 / ((1 - f) + f / p) |
| 55 | +
|
| 56 | + Solve for f: |
| 57 | + 1/S = 1 - f + f/p |
| 58 | + f = (1 - 1/S) / (1 - 1/p) |
| 59 | +
|
| 60 | + Only meaningful for p > 1. |
| 61 | + """ |
| 62 | + S = np.array(speedup, dtype=float) |
| 63 | + p = np.array(procs, dtype=float) |
| 64 | + return (1.0 - 1.0 / S) / (1.0 - 1.0 / p) |
| 65 | + |
| 66 | + |
| 67 | +def amdahl_speedup(p, f): |
| 68 | + p = np.array(p, dtype=float) |
| 69 | + return 1.0 / ((1.0 - f) + f / p) |
| 70 | + |
| 71 | + |
| 72 | +def print_table(name, procs, times, speedup, efficiency): |
| 73 | + print(f"\n{name} results (1M-sample strong-scaling)") |
| 74 | + print(f"{'p':>3} {'time(s)':>12} {'speedup':>12} {'efficiency':>12}") |
| 75 | + for p, t, s, e in zip(procs, times, speedup, efficiency): |
| 76 | + print(f"{p:3d} {t:12.6f} {s:12.3f} {e:12.3f}") |
| 77 | + |
| 78 | + |
| 79 | +def print_weak_scaling(name, procs, problem_sizes, times): |
| 80 | + if not problem_sizes or not times: |
| 81 | + print(f"\nNo weak-scaling data provided for {name}.") |
| 82 | + return |
| 83 | + |
| 84 | + procs = np.array(procs, dtype=int) |
| 85 | + problem_sizes = np.array(problem_sizes, dtype=float) |
| 86 | + times = np.array(times, dtype=float) |
| 87 | + |
| 88 | + print(f"\n{name} weak scaling (problem size grows with p)") |
| 89 | + print(f"{'p':>3} {'problem size':>15} {'time(s)':>12} {'N/p':>15}") |
| 90 | + for p, n, t in zip(procs, problem_sizes, times): |
| 91 | + print(f"{p:3d} {int(n):15d} {t:12.6f} {n/p:15.2f}") |
| 92 | + |
| 93 | + |
| 94 | +def choose_optimal(procs, speedup, efficiency): |
| 95 | + procs = np.array(procs, dtype=int) |
| 96 | + speedup = np.array(speedup, dtype=float) |
| 97 | + efficiency = np.array(efficiency, dtype=float) |
| 98 | + |
| 99 | + p_best_speedup = int(procs[np.argmax(speedup)]) |
| 100 | + p_best_eff = int(procs[np.argmax(efficiency)]) |
| 101 | + |
| 102 | + return p_best_speedup, p_best_eff |
| 103 | + |
| 104 | + |
| 105 | +def main(): |
| 106 | + # ---- sanity checks ---- |
| 107 | + if serial_time <= 0.0: |
| 108 | + raise ValueError("Please set 'serial_time' to a positive value.") |
| 109 | + |
| 110 | + if len(omp_threads) != len(omp_times): |
| 111 | + raise ValueError("omp_threads and omp_times must have the same length.") |
| 112 | + if len(mpi_procs) != len(mpi_times): |
| 113 | + raise ValueError("mpi_procs and mpi_times must have the same length.") |
| 114 | + |
| 115 | + # ---- OMP analysis ---- |
| 116 | + omp_speedup, omp_eff = compute_speedup_efficiency( |
| 117 | + serial_time, omp_times, omp_threads |
| 118 | + ) |
| 119 | + print_table("OMP", omp_threads, omp_times, omp_speedup, omp_eff) |
| 120 | + |
| 121 | + # estimate parallel fraction from runs with p > 1 |
| 122 | + if len(omp_threads) > 1: |
| 123 | + p_gt1 = [p for p in omp_threads if p > 1] |
| 124 | + s_gt1 = [s for p, s in zip(omp_threads, omp_speedup) if p > 1] |
| 125 | + if p_gt1: |
| 126 | + f_omp = estimate_parallel_fraction(s_gt1, p_gt1) |
| 127 | + print(f"\nEstimated OMP parallel fraction f (Amdahl) ~ {np.mean(f_omp):.3f}") |
| 128 | + |
| 129 | + # ---- MPI analysis ---- |
| 130 | + mpi_speedup, mpi_eff = compute_speedup_efficiency( |
| 131 | + serial_time, mpi_times, mpi_procs |
| 132 | + ) |
| 133 | + print_table("MPI", mpi_procs, mpi_times, mpi_speedup, mpi_eff) |
| 134 | + |
| 135 | + if len(mpi_procs) > 1: |
| 136 | + p_gt1 = [p for p in mpi_procs if p > 1] |
| 137 | + s_gt1 = [s for p, s in zip(mpi_procs, mpi_speedup) if p > 1] |
| 138 | + if p_gt1: |
| 139 | + f_mpi = estimate_parallel_fraction(s_gt1, p_gt1) |
| 140 | + print(f"\nEstimated MPI parallel fraction f (Amdahl) ~ {np.mean(f_mpi):.3f}") |
| 141 | + |
| 142 | + # ---- Optimal core/thread choice (part (b)) ---- |
| 143 | + omp_best_S, omp_best_E = choose_optimal(omp_threads, omp_speedup, omp_eff) |
| 144 | + mpi_best_S, mpi_best_E = choose_optimal(mpi_procs, mpi_speedup, mpi_eff) |
| 145 | + |
| 146 | + print("\n=== Suggested 'optimal' choices ===") |
| 147 | + print(f"OMP: max speedup at {omp_best_S} threads; " |
| 148 | + f"max efficiency at {omp_best_E} threads.") |
| 149 | + print(f"MPI: max speedup at {mpi_best_S} processes; " |
| 150 | + f"max efficiency at {mpi_best_E} processes.") |
| 151 | + |
| 152 | + # ---- Weak scaling (part (c)) ---- |
| 153 | + print_weak_scaling("OMP", omp_threads, weak_problem_sizes_omp, weak_times_omp) |
| 154 | + print_weak_scaling("MPI", mpi_procs, weak_problem_sizes_mpi, weak_times_mpi) |
| 155 | + |
| 156 | + # ==================== PLOTS ==================== |
| 157 | + |
| 158 | + # Strong scaling: speedup |
| 159 | + plt.figure() |
| 160 | + plt.plot(omp_threads, omp_speedup, marker='o', label="OMP") |
| 161 | + plt.plot(mpi_procs, mpi_speedup, marker='s', label="MPI") |
| 162 | + plt.xlabel("Number of threads / processes (p)") |
| 163 | + plt.ylabel("Observed speedup S(p)") |
| 164 | + plt.title("Speedup vs. p (strong scaling, 1M samples)") |
| 165 | + plt.grid(True) |
| 166 | + plt.legend() |
| 167 | + plt.tight_layout() |
| 168 | + plt.savefig("speedup_strong_scaling.png") |
| 169 | + |
| 170 | + # Strong scaling: efficiency |
| 171 | + plt.figure() |
| 172 | + plt.plot(omp_threads, omp_eff, marker='o', label="OMP") |
| 173 | + plt.plot(mpi_procs, mpi_eff, marker='s', label="MPI") |
| 174 | + plt.xlabel("Number of threads / processes (p)") |
| 175 | + plt.ylabel("Efficiency E(p) = S(p)/p") |
| 176 | + plt.title("Efficiency vs. p (strong scaling, 1M samples)") |
| 177 | + plt.grid(True) |
| 178 | + plt.legend() |
| 179 | + plt.tight_layout() |
| 180 | + plt.savefig("efficiency_strong_scaling.png") |
| 181 | + |
| 182 | + # Weak scaling plots (if data exist) |
| 183 | + if weak_problem_sizes_omp and weak_times_omp: |
| 184 | + plt.figure() |
| 185 | + plt.plot(omp_threads, weak_times_omp, marker='o') |
| 186 | + plt.xlabel("Number of threads (p)") |
| 187 | + plt.ylabel("Runtime (s)") |
| 188 | + plt.title("OMP weak scaling runtime") |
| 189 | + plt.grid(True) |
| 190 | + plt.tight_layout() |
| 191 | + plt.savefig("omp_weak_scaling_runtime.png") |
| 192 | + |
| 193 | + if weak_problem_sizes_mpi and weak_times_mpi: |
| 194 | + plt.figure() |
| 195 | + plt.plot(mpi_procs, weak_times_mpi, marker='o') |
| 196 | + plt.xlabel("Number of processes (p)") |
| 197 | + plt.ylabel("Runtime (s)") |
| 198 | + plt.title("MPI weak scaling runtime") |
| 199 | + plt.grid(True) |
| 200 | + plt.tight_layout() |
| 201 | + plt.savefig("mpi_weak_scaling_runtime.png") |
| 202 | + |
| 203 | + plt.show() |
| 204 | + |
| 205 | + |
| 206 | +if __name__ == "__main__": |
| 207 | + main() |
0 commit comments