- Added benchmark_transpose.f90 to measure blocked transpose performance. - Added tune_blocksize.py to auto-detect grid sizes, benchmark valid block size candidates, and apply the optimal value. - Updated makefile to support benchmark targets and default BLOCKSIZE=16 based on autotuning. - Saved tuning results and system specifications to tuning_results.md.
216 lines
7.9 KiB
Python
Executable file
216 lines
7.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import struct
|
|
import subprocess
|
|
import time
|
|
import re
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Auto-tune transpose block size for m_calculate.f90")
|
|
parser.add_argument("--nxp", type=int, help="Override grid size in X direction")
|
|
parser.add_argument("--nyp", type=int, help="Override grid size in Y direction")
|
|
parser.add_argument("--nzp", type=int, help="Override grid size in Z direction")
|
|
parser.add_argument("--iters", type=int, default=5, help="Number of iterations for benchmark run")
|
|
parser.add_argument("--apply", action="store_true", help="Apply the optimal block size to the makefile")
|
|
return parser.parse_args()
|
|
|
|
def read_fortran_grid_sizes(filepath):
|
|
try:
|
|
with open(filepath, 'rb') as f:
|
|
# First 4 bytes is the record length
|
|
len_bytes = f.read(4)
|
|
if len(len_bytes) < 4:
|
|
return None
|
|
rec_len = struct.unpack('i', len_bytes)[0]
|
|
|
|
# Read the record data
|
|
rec_data = f.read(rec_len)
|
|
if len(rec_data) < rec_len:
|
|
return None
|
|
|
|
# Record structure: tnow (double), nx (int64), ny (int64), nz (int64)
|
|
# Since compiler has -fdefault-integer-8, these are 64-bit integers (8 bytes)
|
|
# Format: 'dqqq' -> double, int64, int64, int64
|
|
tnow, nx, ny, nz = struct.unpack('dqqq', rec_data[:32])
|
|
return nx, ny, nz
|
|
except Exception as e:
|
|
print(f"Error reading grid size from {filepath}: {e}")
|
|
return None
|
|
|
|
def detect_grid_sizes():
|
|
# Try current directory first, then parent directory
|
|
search_dirs = [".", "..", "../scratch/ic1_regression_test", "scratch/ic1_regression_test"]
|
|
|
|
intro_file = None
|
|
intro_dir = None
|
|
for d in search_dirs:
|
|
p = os.path.join(d, "post-edge-cold-bc-hybrid-intro")
|
|
if os.path.exists(p):
|
|
intro_file = p
|
|
intro_dir = d
|
|
break
|
|
|
|
if intro_file:
|
|
print(f"Found intro config file at {intro_file}")
|
|
startnum = None
|
|
try:
|
|
with open(intro_file, 'r') as f:
|
|
for line in f:
|
|
if 'startnum' in line:
|
|
# Extract the first integer in the line
|
|
match = re.search(r'\d+', line)
|
|
if match:
|
|
startnum = int(match.group())
|
|
break
|
|
except Exception as e:
|
|
print(f"Error reading startnum from {intro_file}: {e}")
|
|
|
|
if startnum is not None:
|
|
# Search for fort.<startnum> in search_dirs
|
|
for d in search_dirs:
|
|
fort_path = os.path.join(d, f"fort.{startnum}")
|
|
if os.path.exists(fort_path):
|
|
sizes = read_fortran_grid_sizes(fort_path)
|
|
if sizes:
|
|
print(f"Successfully auto-detected grid size from {fort_path}: NX={sizes[0]}, NY={sizes[1]}, NZ={sizes[2]}")
|
|
return sizes
|
|
|
|
# Fallback to default
|
|
print("Could not auto-detect grid sizes. Using defaults: NX=512, NY=256, NZ=256")
|
|
return 512, 256, 256
|
|
|
|
def find_divisors(n):
|
|
divisors = []
|
|
for i in range(1, int(n**0.5) + 1):
|
|
if n % i == 0:
|
|
divisors.append(i)
|
|
if i*i != n:
|
|
divisors.append(n // i)
|
|
return sorted(divisors)
|
|
|
|
def run_command(args, cwd=None):
|
|
res = subprocess.run(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
return res.returncode == 0, res.stdout, res.stderr
|
|
|
|
def update_makefile(best_bs):
|
|
makefile_path = "makefile"
|
|
if not os.path.exists(makefile_path):
|
|
makefile_path = "code/makefile"
|
|
if not os.path.exists(makefile_path):
|
|
makefile_path = "../code/makefile"
|
|
if not os.path.exists(makefile_path):
|
|
print("Error: makefile not found. Cannot apply changes.")
|
|
return False
|
|
|
|
try:
|
|
with open(makefile_path, 'r') as f:
|
|
content = f.read()
|
|
|
|
new_content = re.sub(r'(BLOCKSIZE\s*\?=\s*)\d+', f'\\g<1>{best_bs}', content)
|
|
|
|
with open(makefile_path, 'w') as f:
|
|
f.write(new_content)
|
|
|
|
print(f"Successfully updated {makefile_path} with BLOCKSIZE={best_bs}")
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error updating makefile: {e}")
|
|
return False
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
# Grid sizes detection
|
|
detected_nx, detected_ny, detected_nz = detect_grid_sizes()
|
|
nx = args.nxp if args.nxp else detected_nx
|
|
ny = args.nyp if args.nyp else detected_ny
|
|
nz = args.nzp if args.nzp else detected_nz
|
|
|
|
print(f"Target grid size for tuning: NX={nx}, NY={ny}, NZ={nz}")
|
|
|
|
# Calculate candidate block sizes (must be divisors of NY)
|
|
all_divisors = find_divisors(ny)
|
|
# Filter to block sizes >= 4 to avoid excessive overhead
|
|
candidates = [d for d in all_divisors if d >= 4]
|
|
|
|
if not candidates:
|
|
print(f"No suitable divisors found for NY={ny}. Testing all divisors: {all_divisors}")
|
|
candidates = all_divisors
|
|
|
|
print(f"Candidate block sizes to test: {candidates}")
|
|
|
|
code_dir = "."
|
|
if not os.path.exists("makefile") and os.path.exists("code/makefile"):
|
|
code_dir = "code"
|
|
|
|
print(f"Using build directory: {os.path.abspath(code_dir)}")
|
|
print(f"Running each benchmark with {args.iters} iterations...")
|
|
print("\n" + "="*80)
|
|
print(f"{'Block Size':^12} | {'Compile Time (s)':^18} | {'Avg Run Time (s)':^20} | {'Status':^12}")
|
|
print("-"*80)
|
|
|
|
results = []
|
|
|
|
for bs in candidates:
|
|
# 1. Compile
|
|
t_start = time.time()
|
|
ok_clean, _, _ = run_command(["make", "clean"], cwd=code_dir)
|
|
if not ok_clean:
|
|
print(f"{bs:^12} | {'-':^18} | {'-':^20} | {'CLEAN FAILED':^12}")
|
|
continue
|
|
|
|
compile_cmd = ["make", "benchmark_transpose", f"BLOCKSIZE={bs}"]
|
|
ok_compile, stdout_c, stderr_c = run_command(compile_cmd, cwd=code_dir)
|
|
t_compile = time.time() - t_start
|
|
|
|
if not ok_compile:
|
|
print(f"{bs:^12} | {t_compile:^18.2f} | {'-':^20} | {'COMP. FAILED':^12}")
|
|
# print(stderr_c)
|
|
continue
|
|
|
|
# 2. Run benchmark
|
|
# benchmark_transpose binary is in code_dir
|
|
binary_path = "./benchmark_transpose"
|
|
run_args = [binary_path, str(nx), str(ny), str(nz), str(args.iters)]
|
|
|
|
ok_run, stdout_r, stderr_r = run_command(run_args, cwd=code_dir)
|
|
if not ok_run:
|
|
print(f"{bs:^12} | {t_compile:^18.2f} | {'-':^20} | {'RUN FAILED':^12}")
|
|
# print(stderr_r)
|
|
continue
|
|
|
|
try:
|
|
# Parse the time output from the program
|
|
avg_time = float(stdout_r.strip())
|
|
print(f"{bs:^12d} | {t_compile:^18.2f} | {avg_time:^20.6f} | {'SUCCESS':^12}")
|
|
results.append((bs, avg_time))
|
|
except ValueError:
|
|
print(f"{bs:^12} | {t_compile:^18.2f} | {'-':^20} | {'PARSE ERROR':^12}")
|
|
# print(f"Output was: {stdout_r}")
|
|
|
|
print("="*80)
|
|
|
|
if not results:
|
|
print("Tuning failed: no successful candidate runs.")
|
|
sys.exit(1)
|
|
|
|
# Find the best block size
|
|
best_bs, best_time = min(results, key=lambda x: x[1])
|
|
print(f"\nOptimal BLOCKSIZE found: {best_bs} with average time of {best_time:.6f} seconds.")
|
|
|
|
# Recommendations
|
|
print("\nTested results:")
|
|
for bs, t in results:
|
|
rel = (t - best_time) / best_time * 100.0
|
|
marker = " (Best)" if bs == best_bs else f" (+{rel:.1f}%)"
|
|
print(f" Block Size {bs:3d}: {t:.6f} s{marker}")
|
|
|
|
if args.apply:
|
|
update_makefile(best_bs)
|
|
else:
|
|
print(f"\nTo apply this block size, run with --apply or manually update 'BLOCKSIZE?={best_bs}' in the makefile.")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|