diff --git a/analysis/fig_milestones.png b/analysis/fig_milestones.png index 625b0c5..6fdff91 100644 Binary files a/analysis/fig_milestones.png and b/analysis/fig_milestones.png differ diff --git a/analysis/fig_roofline.png b/analysis/fig_roofline.png index 59f1af6..bcf45c7 100644 Binary files a/analysis/fig_roofline.png and b/analysis/fig_roofline.png differ diff --git a/analysis/fig_speedup.png b/analysis/fig_speedup.png new file mode 100644 index 0000000..5f577e6 Binary files /dev/null and b/analysis/fig_speedup.png differ diff --git a/analysis/fig_speedup_all.png b/analysis/fig_speedup_all.png new file mode 100644 index 0000000..0c656b6 Binary files /dev/null and b/analysis/fig_speedup_all.png differ diff --git a/analysis/fig_tracks.png b/analysis/fig_tracks.png index 6641be5..f0fd3f5 100644 Binary files a/analysis/fig_tracks.png and b/analysis/fig_tracks.png differ diff --git a/analysis/make_csv.py b/analysis/make_csv.py index 8c4ec0c..dec63fd 100644 --- a/analysis/make_csv.py +++ b/analysis/make_csv.py @@ -27,6 +27,14 @@ def num(pattern, text): #min cycles is the standard way to kill the OS/turbo noise. best = {} +# flop counter is uint32 and wraps at n=10000 (72*n^2+74*n = 7.2e9 > 2^32), so the logged +# W(n)/I(n) are garbage there. compute work ourselves; matches the good logs exactly. +# symmetry variants do half the pairs -> 36 n^2 + 110 n, everything else 72 n^2 + 74 n. +def flops_per_iter(variant, n): + if "symmetry" in variant: + return 36 * n * n + 110 * n + return 72 * n * n + 74 * n + for variant in sorted(os.listdir(ROOT)): logdir = os.path.join(ROOT, variant, "x86") if not os.path.isdir(logdir): @@ -48,20 +56,20 @@ for variant in sorted(os.listdir(ROOT)): cycles = num(r"T\(n\):\s*([\d.]+)", block) if cycles is None: continue - W = num(r"W\(n\):\s*([\d.]+)", block) key = (variant, n) - if key not in best or cycles < best[key][2]: - best[key] = (variant, n, cycles, n_iter, W, - num(r"Q\(n\) per iteration:\s*([\d.]+)", block), - num(r"I\(n\):\s*([\d.]+)", block)) + if key not in best or cycles < best[key][0]: + best[key] = (cycles, n_iter, + num(r"Q\(n\) per iteration:\s*([\d.]+)", block)) with open(OUT, "w", newline="") as f: w = csv.writer(f) w.writerow(["variant", "n", "n_iter", "W", "Q_per_iter", "I", "cycles", "flops_per_cycle"]) for variant, n in sorted(best): - _, _, cycles, n_iter, W, q, I = best[(variant, n)] + cycles, n_iter, q = best[(variant, n)] + w_iter = flops_per_iter(variant, n) + W = w_iter * n_iter + I = w_iter / q fc = W / cycles - w.writerow([variant, n, n_iter, W, q, I, round(cycles, 1), round(fc, 3)]) - + w.writerow([variant, n, n_iter, W, q, round(I, 2), round(cycles, 1), round(fc, 3)]) print("wrote", OUT, "with", len(best), "rows") \ No newline at end of file diff --git a/analysis/plots.py b/analysis/plots.py index e1ec119..cb833dc 100644 --- a/analysis/plots.py +++ b/analysis/plots.py @@ -81,13 +81,14 @@ base = get("variant_baseline") # plot 1: the two precision tracks (this is the main results plot) # double tops out ~7, float goes to 24. also shows the avx2-float stuff already does most of the work tracks = [ - ("variant_inlining_7_SoA_AVX2", "double, AVX2", "#9bbbe0", "--"), + ("variant_baseline", "baseline (scalar)", "#7a7a7a", "-"), + ("variant_AVX512_1", "double, AVX-512 ", "#4f86d6", "-"), + ("variant_inlining_7_SoA_AVX2", "double, AVX-256", "#9bbbe0", "--"), ("variant_inlining_8_SoA_AVX512_polyexp_moreavx", "double, AVX-512 + polyexp", "#2f5d9e", "-"), - ("variant_float_inlining_SoA_AVX2_polyexp", "float, AVX2 + polyexp", "#e58aa6", "--"), - ("variant_float_inlining_SoA_AVX2_polyexp_rsqrt_loopopt", "float, AVX2 + polyexp + rsqrt", "#d05a82", "--"), + ("variant_float_inlining_SoA_AVX2_polyexp", "float, AVX-256 + polyexp", "#e58aa6", "--"), + ("variant_float_inlining_SoA_AVX2_polyexp_rsqrt_loopopt", "float, AVX-256 + polyexp + rsqrt", "#d05a82", "--"), ("variant_float_inlining_SoA_AVX512_polyexp_moreavx_ilp_2_rsqrt_morersqrt", "float, AVX-512 + rsqrt + ILP (best)", maroon, "-"), ] - fig, ax = plt.subplots(figsize=(9,4.7)) ends = [] for v,lab,c,ls in tracks: @@ -144,43 +145,53 @@ plt.tight_layout() plt.savefig("analysis/fig_ladder.png", dpi=170, bbox_inches="tight") -# plot3 roofline -# every variant has the same I at a given n (W and Q dont change between variants), so the -# points just stack vertically. we're miles to the right of the ridge -> compute bound. -fig,ax = plt.subplots(figsize=(9,4.7)) -bw = 60 # bytes/cycle, ~120 GB/s lpddr5x / 2GHz. rough -xx = np.logspace(-1, 3.6, 200) -ax.plot(xx, np.minimum(bw*xx, PEAK_F), color="#cccccc", lw=1.2) +# plot 3: roofline (n = 5000). same 7 variants as fig_tracks, same colors, inline labels. +# float variants move HALF the bytes -> 2x the operational intensity of the double ones +# (I_double ≈ 2.8k, I_float ≈ 5.6k), so each point uses its OWN I. +# memory roof from a measured single-thread read: sysbench 37618 MiB/s -> ~19.7 bytes/cycle @ 2 GHz. +def getI(v, n=5000): + return df[(df.variant==v) & (df.n==n)]["I"].values[0] +bw = 37618.01 * 1024**2 / 2e9 # MiB/s -> bytes/cycle at 2 GHz (≈ 19.72) -for pk,t in [(SCALAR,"scalar (4)"),(PEAK_D,"double (16)"),(PEAK_F,"float (32)")]: - ax.axhline(pk, ls="--", color="#999", lw=1.1) - ax.text(0.11, pk*1.04, t+" peak", fontsize=9.5, color="#888") - -I5000 = df[(df.variant=="variant_baseline") & (df.n==5000)]["I"].values[0] -pts = [ - ("variant_baseline", "#7a7a7a"), - ("variant_AVX512_1", "#2f9e8f"), - ("variant_inlining_8_SoA_AVX512_polyexp_moreavx", "#e08a1e"), - ("variant_float_inlining_SoA_AVX512_polyexp_moreavx", "#c0436a"), - ("variant_float_inlining_SoA_AVX512_polyexp_moreavx_ilp_2_rsqrt_morersqrt", maroon), +# (variant, label, color, group) group "d" = double column, "f" = float column +roof = [ + ("variant_baseline", "baseline (scalar)", "#7a7a7a", "d"), + ("variant_inlining_7_SoA_AVX2", "double, AVX-256", "#9bbbe0", "d"), + ("variant_AVX512_1", "double, AVX-512 ", "#4f86d6", "d"), + ("variant_inlining_8_SoA_AVX512_polyexp_moreavx", "double, AVX-512 + polyexp", "#2f5d9e", "d"), + ("variant_float_inlining_SoA_AVX2_polyexp", "float, AVX-256 + polyexp", "#e58aa6", "f"), + ("variant_float_inlining_SoA_AVX2_polyexp_rsqrt_loopopt", "float, AVX2 + polyexp + rsqrt", "#d05a82", "f"), + ("variant_float_inlining_SoA_AVX512_polyexp_moreavx_ilp_2_rsqrt_morersqrt", "float, AVX-512 + rsqrt + ILP (best)", maroon, "f"), ] -for v,c in pts: - ax.plot(I5000, get(v), "o", ms=9, color=c, mec="white", mew=1) +LABX = {"d": 2400, "f": 4900} # label right-edge sits just left of each column +fig, ax = plt.subplots(figsize=(9.5,5.0)) +xx = np.logspace(-1, 4.2, 200) +ax.plot(xx, np.minimum(bw*xx, PEAK_F), color="#bbbbbb", lw=1.4) +ax.text(0.115, 0.85, "mem roof: DRAM read ≈ 37.6 GiB/s (≈19.7 B/cycle)", fontsize=8.3, color="#9a9a9a", ha="left") -ax.text(I5000, 1.3, "n=5000", fontsize=10, color="#555") +for pk,t in [(SCALAR,"scalar (4)"), (PEAK_D,"double (16)"), (PEAK_F,"float (32)")]: + ax.axhline(pk, ls="--", color="#aaaaaa", lw=1.0) + ax.text(0.11, pk*1.05, t+" peak", fontsize=9, color="#888") + +for v,lab,c,grp in roof: + x,y = getI(v), get(v) + ax.plot(x, y, "o", ms=10, color=c, mec="white", mew=1.4, zorder=5) + t = ax.text(LABX[grp], y, lab, color=c, fontsize=8, fontweight="bold", ha="right", va="center", zorder=6) + t.set_path_effects([pe.withStroke(linewidth=2.5, foreground="white")]) + +ax.text(2812, 0.74, "I ≈ 2.8k", fontsize=8, color="#777", ha="center", va="top") +ax.text(5625, 0.74, "I ≈ 5.6k", fontsize=8, color="#777", ha="center", va="top") ax.set_xscale("log"); ax.set_yscale("log") -ax.set_xlim(0.1, 6500); ax.set_ylim(0.7, 45) +ax.set_xlim(0.1, 16000); ax.set_ylim(0.6, 46) ax.set_xlabel("operational intensity I [flops/byte]") ax.set_ylabel("performance [flops/cycle]") -#ax.set_title("roofline: deeply compute-bound (I grows with n; work O(n^2), data O(n))", loc="left", fontsize=12) ax.grid(color="#ededed") ax.spines["top"].set_visible(False); ax.spines["right"].set_visible(False) plt.tight_layout() plt.savefig("analysis/fig_roofline.png", dpi=170, bbox_inches="tight") - # plot 4: exp vs the polynomial approx (just to show its basically exact) x = np.linspace(-6, 0, 300) ex = np.exp(x) @@ -205,7 +216,7 @@ plt.savefig("analysis/fig_exp.png", dpi=170, bbox_inches="tight") # "above" or "below" -> alternate the crowded float labels above/below their line. milestones = [ ("variant_baseline", "baseline", "#7a7a7a", "-", None, "above"), - ("variant_cutoff_callsite", "baseline + cutoff (best scalar)", "#9e8f2f", "-", None, "below"), + #("variant_cutoff_callsite", "baseline + cutoff (best scalar)", "#9e8f2f", "-", None, "below"), ("variant_inlining_7_SoA_AVX2", "AVX2 (double)", "#3b6fb0", "--", None, "above"), ("variant_inlining_7_SoA_AVX2_polyexp", "AVX2 + polyexp (double)", "#2f9e8f", "--", None, "below"), ("variant_inlining_8_SoA_AVX512_polyexp_moreavx", "AVX-512 (double)", "#2f5d9e", "-", None, "below"), @@ -244,4 +255,47 @@ plt.savefig("analysis/fig_milestones.png", dpi=170, bbox_inches="tight") #plt.show() -print("done") \ No newline at end of file +print("done") + +# plot 6: wall-clock speedup over baseline (runtime, not performance). +# the fair view for optimizations that change the flop count -- cutoff skips ~40% of the +# inner loop, symmetry does half the pairs -- so flops/cycle misrepresents them. T_base / +# T_variant compares time-to-solve the same n regardless of how many flops each one does. +# curated to the milestones that carry the story + symmetry as the cautionary line. +basec = df[df.variant == "variant_baseline"].set_index("n")["cycles"].to_dict() +def speedline(v): + s = df[df.variant == v].sort_values("n") + ns = [n for n in s.n.values if n in basec] # baseline has no n=10000 run + sp = [basec[n] / s[s.n == n]["cycles"].values[0] for n in ns] + return np.array(ns, float), np.array(sp, float) + +# (variant, label, color, linestyle, xpos|None, side) +speed = [ + ("variant_cutoff_callsite", "scalar + cutoff (approx)", "#9e8f2f", "-", 260, "above"), + ("variant_inlining_7_symmetry_2", "symmetry (half the pairs)", "#cf2b2b", "-", 320, "below"), + ("variant_inlining_7_SoA_AVX2", "double, AVX2", "#9bbbe0", "--", 650, "below"), + ("variant_inlining_8_SoA_AVX512_polyexp_moreavx", "double, AVX-512 + polyexp", "#2f5d9e", "-", 430, "above"), + ("variant_float_inlining_SoA_AVX2_polyexp_rsqrt_loopopt", "float, AVX2 + polyexp + rsqrt", "#d05a82", "--", 950, "below"), + ("variant_float_inlining_SoA_AVX512_polyexp_moreavx_ilp_2_rsqrt_morersqrt", "float, AVX-512 + rsqrt + ILP (best)", maroon, "-", 680, "above"), +] +fig, ax = plt.subplots(figsize=(9,4.7)) +ends = [] +for v,lab,c,ls,xpos,side in speed: + x,y = speedline(v) + if len(x) == 0: + print("skip (not in csv):", v); continue + ax.plot(x, y, ls, color=c, marker="o", ms=4, lw=2.4 if c==maroon else 1.8) + ends.append((x, y, lab, c, xpos, side)) +ax.axhline(1, ls=":", color="#999", lw=1.3) +ax.text(52, 1.03, "baseline (1x)", fontsize=9, color="#888") +ax.set_xscale("log"); ax.set_yscale("log") +ax.set_xticks([50,100,200,500,1000,2000,5000]); ax.set_xticklabels(["50","100","200","500","1k","2k","5k"]) +ax.set_yticks([1,2,5,10,20]); ax.set_yticklabels(["1x","2x","5x","10x","20x"]) +ax.set_xlim(46,7000); ax.set_ylim(0.8,28) +ax.set_xlabel("number of pedestrians n") +ax.set_ylabel("speedup over baseline (wall-clock cycles)") +ax.grid(color="#e9e9e9") +ax.spines["top"].set_visible(False); ax.spines["right"].set_visible(False) +plt.tight_layout() +label_lines_above(ax, ends, fontsize=7.5, dy_frac=0.02, halo=2) +plt.savefig("analysis/fig_speedup.png", dpi=170, bbox_inches="tight") \ No newline at end of file