diff --git a/analysis/fig_exp.png b/analysis/fig_exp.png new file mode 100644 index 0000000..fb9c537 Binary files /dev/null and b/analysis/fig_exp.png differ diff --git a/analysis/fig_ladder.png b/analysis/fig_ladder.png new file mode 100644 index 0000000..49d9d19 Binary files /dev/null and b/analysis/fig_ladder.png differ diff --git a/analysis/fig_roofline.png b/analysis/fig_roofline.png new file mode 100644 index 0000000..59f1af6 Binary files /dev/null and b/analysis/fig_roofline.png differ diff --git a/analysis/fig_tracks.png b/analysis/fig_tracks.png new file mode 100644 index 0000000..4b03f75 Binary files /dev/null and b/analysis/fig_tracks.png differ diff --git a/analysis/make_csv.py b/analysis/make_csv.py new file mode 100644 index 0000000..8c4ec0c --- /dev/null +++ b/analysis/make_csv.py @@ -0,0 +1,67 @@ +import os +import re +import csv + +# folder that holds one subfolder per variant, each with an x86/ dir full of *.log files +ROOT = "measurements" +OUT = "analysis/results.csv" + +# the benchmark prints one block per problem size, looks like: +# === N_P=500 N_ITER=200 === +# W(n) per iteration: ... +# Q(n) per iteration: ... +# I(n): ... +# W(n): ... +# T(n): ... <- cycles +# P(n): ... <- flops/cycle +# so I split each log on the "=== N_P=.. ===" lines and read the fields out of each block. +size_line = re.compile(r"=== N_P=(\d+) N_ITER=(\d+) ===") + +def num(pattern, text): + m = re.search(pattern, text) + if m: + return float(m.group(1)) + return None + +#we run every variant a few times (1.log, 2.log, ...). keep the fastest run per size, +#min cycles is the standard way to kill the OS/turbo noise. +best = {} + +for variant in sorted(os.listdir(ROOT)): + logdir = os.path.join(ROOT, variant, "x86") + if not os.path.isdir(logdir): + continue + + for fn in os.listdir(logdir): + if not fn.endswith(".log"): + continue + text = open(os.path.join(logdir, fn)).read() + + marks = list(size_line.finditer(text)) + for i, m in enumerate(marks): + n = int(m.group(1)) + n_iter = int(m.group(2)) + start = m.end() + stop = marks[i + 1].start() if i + 1 < len(marks) else len(text) + block = text[start:stop] + + 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)) + +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)] + fc = W / cycles + w.writerow([variant, n, n_iter, W, q, I, 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 new file mode 100644 index 0000000..0669f1f --- /dev/null +++ b/analysis/plots.py @@ -0,0 +1,144 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +#import seaborn as sns # tried it, the default look was worse + +df = pd.read_csv("analysis/results.csv") + +# peaks for the ryzen ai 7 350 (zen5) at 2GHz. 2 fma units and the datapath is 256 bit +# -> scalar 4, double 16, float 32 flops/cycle +SCALAR = 4 +PEAK_D = 16 +PEAK_F = 32 +maroon = "#A71D5B" + +def get(v, n=5000): + return df[(df.variant==v) & (df.n==n)]["flops_per_cycle"].values[0] + +def getline(v): + s = df[df.variant == v].sort_values("n") + return s.n.values, s["flops_per_cycle"].values + +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_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_AVX512_polyexp_moreavx_ilp_2_rsqrt_morersqrt", "float, AVX-512 + rsqrt + ILP (best)", maroon, "-"), +] + +fig, ax = plt.subplots(figsize=(9,4.7)) +for v,lab,c,ls in tracks: + x,y = getline(v) + ax.plot(x, y, ls, color=c, marker="o", ms=4, lw= 2.4 if c==maroon else 1.8, label=lab) + +for yv,t in [(PEAK_D,"double peak (16)"), (PEAK_F,"float peak (32)")]: + ax.axhline(yv, ls=":", color="#999", lw=1.2) + ax.text(52, yv*1.03, t, fontsize=9.5, color="#888") + +ax.set_xscale("log"); ax.set_yscale("log") +ax.set_xticks([50,100,200,500,1000,2000,5000,10000]) +ax.set_xticklabels(["50","100","200","500","1k","2k","5k","10k"]) +ax.set_yticks([1,2,4,8,16,32]); ax.set_yticklabels(["1","2","4","8","16","32"]) +ax.set_xlim(46,13000); ax.set_ylim(1,40) +ax.set_xlabel("number of pedestrians n") +ax.set_ylabel("performance [flops/cycle]") +ax.grid(color="#e9e9e9") +ax.spines["top"].set_visible(False); ax.spines["right"].set_visible(False) +ax.legend(fontsize=9, loc="lower left", framealpha=.95) +plt.tight_layout() +plt.savefig("analysis/fig_tracks.png", dpi=170, bbox_inches="tight") + + +# plot 2: all te milestones + the symmetry one that got slower +bars = [ + ("baseline", "variant_baseline"), + ("AVX2 (double)", "variant_inlining_7_SoA_AVX2"), + ("AVX-512 (double)", "variant_avx512_1"), + ("+ polyexp", "variant_inlining_8_SoA_AVX512_polyexp_moreavx"), + ("+ float", "variant_float_inlining_SoA_AVX512_polyexp_moreavx"), + ("+ rsqrt & ILP", "variant_float_inlining_SoA_AVX512_polyexp_moreavx_ilp_2_rsqrt_morersqrt"), + ("symmetry\n(half the pairs)", "variant_inlining_7_symmetry_2"), +] +names = [b[0] for b in bars] +spd = [get(b[1])/base for b in bars] +cols = ["#7a7a7a","#3b6fb0","#2f9e8f","#e08a1e","#c0436a", maroon, "#cf2b2b"] + +fig,ax = plt.subplots(figsize=(9,4.7)) +yp = np.arange(len(names))[::-1] +ax.barh(yp, spd, color=cols, height=0.66) +for yy,s in zip(yp,spd): + ax.text(s+0.25, yy, str(round(s,1))+"x", va="center", fontsize=11, fontweight="bold", + color = "#cf2b2b" if s<1 else "#222") +ax.axvline(1, ls="--", color="#888", lw=1) +ax.set_yticks(yp); ax.set_yticklabels(names) +ax.set_xlim(0,26) +ax.set_xlabel("speedup over baseline (n=5000)") +#ax.set_title("optimization milestones (and the one that backfired)", loc="left", fontsize=13) +ax.grid(axis="x", color="#eee"); ax.grid(axis="y", visible=False) +ax.spines["top"].set_visible(False); ax.spines["right"].set_visible(False) +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) + +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), +] +for v,c in pts: + ax.plot(I5000, get(v), "o", ms=9, color=c, mec="white", mew=1) + + +ax.text(I5000, 1.3, "n=5000", fontsize=10, color="#555") + +ax.set_xscale("log"); ax.set_yscale("log") +ax.set_xlim(0.1, 6500); ax.set_ylim(0.7, 45) +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) +co = np.polyfit(x, ex, 5) # degree 5 same as in the kernel +ap = np.polyval(co, x) + +fig,ax = plt.subplots(figsize=(4.6,3.7)) +ax.plot(x, ex, color="#222", lw=2.4, label="exp(x)") +ax.plot(x, ap, "--", color=maroon, lw=2, label="poly approx") +ax.set_xlabel("x") +ax.set_title("exp -> polynomial", loc="left", fontsize=12) +ax.grid(color="#ededed") +ax.spines["top"].set_visible(False); ax.spines["right"].set_visible(False) +ax.legend(frameon=False, fontsize=10) +plt.tight_layout() +plt.savefig("analysis/fig_exp.png", dpi=170, bbox_inches="tight") + +#plt.show() +print("done") \ No newline at end of file