Files
TheRock/rocm-libraries/projects/rocprim/scripts/autotune-search/main.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

407 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3
# MIT License
#
# Copyright (c) Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from typing import Union, List
import argparse
import glob
import itertools
import json
import logging
import multiprocessing as mp
import os
import pathos.multiprocessing as pamp
import rich.logging
import rich.progress
import scipy.optimize
import shutil
import subprocess
parameter_spaces = {
"device_segmented_radix_sort_keys": {
"benchmark": "benchmark_device_segmented_radix_sort_keys",
"types": {
"KeyType": [
"int64_t",
"int",
"short",
"int8_t",
"double",
"float",
"rocprim::half",
],
},
"params": {
"RadixBits": [6, 7, 8],
"BlockSize": [256],
"ItemsPerThread": [7, 8, 13, 16, 17],
"WarpSmallLWS": [8, 16, 32, 64],
"WarpSmallIPT": [2, 4, 8],
"WarpSmallBS": [256],
"WarpPartition": [4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096],
"WarpMediumLWS": [16, 32, 64],
"WarpMediumIPT": [4, 8, 16],
"WarpMediumBS": [256],
},
},
"device_segmented_radix_sort_pairs": {
"benchmark": "benchmark_device_segmented_radix_sort_pairs",
"types": {
"KeyType": [
"int64_t",
"int",
"short",
"int8_t",
"double",
"float",
"rocprim::half",
],
"ValueType": [
"int64_t",
"int",
"short",
"int8_t",
],
},
"params": {
"RadixBits": [6, 7, 8],
"BlockSize": [256],
"ItemsPerThread": [7, 8, 13, 16, 17],
"WarpSmallLWS": [8, 16, 32, 64],
"WarpSmallIPT": [2, 4, 8],
"WarpSmallBS": [256],
"WarpPartition": [4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096],
"WarpMediumLWS": [16, 32, 64],
"WarpMediumIPT": [4, 8, 16],
"WarpMediumBS": [256],
},
},
}
def get_result_from_json(filename: os.PathLike) -> Union[float, int]:
'''
Get the result from the benchmark json.
'''
with open(filename, 'r') as file:
data = json.load(file)
try:
return float(data['benchmarks'][0]['bytes_per_second'])
except Exception as e:
log.error('Could not extract \'bytes_per_second\' from JSON!')
raise e
def merge_jsons(source_filenames: List[os.PathLike], target_filename: os.PathLike) -> None:
'''
Merges benchmark JSONs. This is used to collect the singular results generated from the various
benchmark runs.
'''
merged = {
'context': {},
'benchmarks': [],
}
# collect jsons
for filename in source_filenames:
with open(filename, 'r') as file:
try:
data = json.load(file)
except json.decoder.JSONDecodeError as e:
log.warning(f'Skipping file \'{filename}\' because of error: {e}')
# HACK: we reuse the last context since we can only have one
merged['context'] = data['context']
# append benchmark data
merged['benchmarks'].extend(data['benchmarks'])
# write out file
with open(target_filename, 'w') as file:
json.dump(merged, file, indent=2)
def combine(alg_name: str, arch: str):
'''
combine()
'''
script_dir = os.path.dirname(os.path.realpath(__file__))
result_dir = os.path.join(script_dir, 'artifacts')
alg_space = parameter_spaces[alg_name]
build_target = alg_space['benchmark']
merge_jsons(
glob.glob(os.path.join(result_dir, f'{arch}_{build_target}_*.json')),
os.path.join(result_dir, f'{arch}_{build_target}.json'),
)
def tune_alg(alg_name: str, arch: str, max_samples: int, num_workers: int, size: int, trials: int) -> None:
'''
The core tuning procedure. This tunes a single algorithm for multiple types.
'''
# get the context of the tuning run
alg_space = parameter_spaces[alg_name]
build_target = alg_space['benchmark']
# types to tune, this can be a product of multiple types
types = [
dict(zip(alg_space['types'], ts))
for ts in itertools.product(
*[alg_space['types'][type] for type in alg_space['types']]
)
]
# generate bounds by normalizing the parameter space from discrete to real numbers (relaxation)
bounds = dict(zip(alg_space['params'], ((0, 1) for _ in alg_space['params'])))
# define a utility function to access parameters in 'alg_space'
def param_from_normalized(name: str, value: float) -> str:
'''
Internal which maps a continious named parameter in [0; 1] to it's discrete value.
'''
# get the list of discrete values
params = alg_space['params'][name]
# get the index, make sure we're not out-of-bounds when value is 1.0
index = min(int(value * len(params)), len(params) - 1)
try:
return str(params[index])
except IndexError as e:
log.error(
f"Could not find parameter '{name}' at '{index}' derived from value '{value}' in {params}."
)
raise e
def tune_type(type: str) -> None:
cache = {}
def sample(xs: List[float]) -> Union[float, int]:
# each worker should get their own build dir
build_dir = os.path.join(source_dir, f'build/tune-{worker_id}')
# delete *.parallel folder
try:
# HACK: we just delete the benchmark folder because it's easier
shutil.rmtree(os.path.join(build_dir, 'benchmark'))
except FileNotFoundError:
# if the tree doesn't exist we don't have to remove it :)
pass
tune_param_names = list(type.keys()) + list(alg_space['params'])
tune_param_vals = list(type.values()) + [
param_from_normalized(name, value)
for (name, value) in zip(alg_space['params'].keys(), xs)
]
result_id = '_'.join(tune_param_vals)
if result_id in cache:
log.info(f'[{worker_id}] Skipped already computed result!')
return cache[result_id]
result_filename = f'{arch}_{build_target}_{result_id}.json'
tune_param_names = ';'.join(tune_param_names)
tune_param_vals = ';'.join(tune_param_vals)
# CMake configure
log.info(f'[{worker_id}] Configuring: {result_id}')
configure = subprocess.call(
[
'cmake',
'-S',
'.',
'-B',
build_dir,
'-GNinja',
'-DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++',
'-DBUILD_BENCHMARK=ON',
'-DBENCHMARK_CONFIG_TUNING=ON',
f'-DAMDGPU_TARGETS={arch}',
f'-DBENCHMARK_TUNE_PARAM_NAMES={tune_param_names}',
f'-DBENCHMARK_TUNE_PARAMS={tune_param_vals}',
],
cwd=source_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if configure != 0:
cache[result_id] = 0.0
return 0.0
# Build target
log.info(f'[{worker_id}] Building: {result_id}')
build = subprocess.call(
['cmake', '--build', '.', '--target', build_target],
cwd=build_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if build != 0:
cache[result_id] = 0.0
result_context = {
'config': dict(
(
(name, param_from_normalized(name, val))
for name, val in zip(alg_space['params'], xs)
)
)
}
log.debug(json.dumps(result_context, indent=2))
return 0.0
# Run benchmark
gpu_lock.acquire()
try:
log.info(f'[{worker_id}] Benchmarking: {result_id}')
bench = subprocess.call(
[
os.path.join(build_dir, 'benchmark', build_target),
'--seed',
'random', # Random is better... I think? Otherwise we might overfit.
'--size',
f'{size}',
'--trials',
f'{trials}',
f'--benchmark_out={result_filename}',
],
cwd=result_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=120,
)
if bench != 0:
cache[result_id] = 0.0
return 0.0
result_value = get_result_from_json(
os.path.join(result_dir, result_filename)
)
log.info(f'[{worker_id}] Completed: {result_id} @ {result_value / 1e9:.3f} GB/s')
finally:
gpu_lock.release()
result_context = {
'config': dict(
(
(name, param_from_normalized(name, val))
for name, val in zip(alg_space['params'], xs)
)
),
'bytes_per_second': result_value,
}
log.debug(json.dumps(result_context, indent=2))
cache[result_id] = -result_value
# scipy.optimize does minimization, negate result for maximize
return -result_value
# Dual annealing is very good for tuning. See:
# - 'Benchmarking optimization algorithms for auto-tuning GPU kernels' by Schoonhoven et al, 2022.
# - 'A methodology for comparing optimization algorithms for auto-tuning' by Willemsen et al, 2024.
scipy.optimize.dual_annealing(
sample, bounds=bounds.values(), maxfun=max_samples
)
script_dir = os.path.dirname(os.path.realpath(__file__))
source_dir = os.path.join(script_dir, '../..')
result_dir = os.path.join(script_dir, 'artifacts')
os.makedirs(result_dir, exist_ok=True)
def pool_init(worker_ids, lock):
global worker_id, gpu_lock
gpu_lock = lock
worker_id = worker_ids.get(False)
man = mp.Manager()
# create queue to distrubute worker ids
worker_ids = man.Queue(num_workers)
for i in range(num_workers):
worker_ids.put(i)
# 'pathos' is needed to pickle local scopes to workers
with pamp.Pool(
processes=num_workers,
initializer=pool_init,
initargs=[worker_ids, mp.Lock()],
) as pool:
pool.map(tune_type, types)
# We're done with tuning this entire algorithm, collect them into a single file!
merge_jsons(
glob.glob(os.path.join(result_dir, f'{arch}_{build_target}_*.json')),
os.path.join(result_dir, f'{arch}_{build_target}.json'),
)
parser = argparse.ArgumentParser(
prog='autotune-search',
description='config tuning using local search',
)
parser.add_argument('targets',metavar='TARGETS', nargs='*', help='target(s) to optimize, seperated by comma')
parser.add_argument('-a', '--arch', default='gfx942', help='architecture to target, e.g. gfx908')
parser.add_argument('-n', '--evals', default=200, help='maximum number of configs being evaluated per type per target')
parser.add_argument('-v', '--verbose', action='store_true', help='verbose output')
parser.add_argument('-w', '--workers', default=8, help='number of workers')
parser.add_argument('-s', '--size', default=33554432, help='input size to use for tuning')
parser.add_argument('-t', '--trials', default=3, help='number of trials per config to test')
parser.add_argument('-c', '--combine', action='store_true', help='skip tuning and combine the results of a previous run for the given targets and architecture')
parser.add_argument('-l', '--list', action='store_true', help='list available targets')
args = parser.parse_args()
if not args.targets:
args.targets = list(parameter_spaces.keys())
if args.list:
for target in parameter_spaces.keys():
print(target)
quit()
log_level = logging.INFO
if args.verbose:
log_level = logging.DEBUG
logging.basicConfig(format='%(message)s', handlers=[rich.logging.RichHandler(rich_tracebacks=True, markup=True)], level=log_level)
log = logging.getLogger('rich')
if args.combine:
for target in args.targets:
combine(alg_name=target, arch=args.arch)
quit()
for target in args.targets:
log.info(f'Tuning {target} for {args.arch} with {int(args.evals)} max evaluations')
tune_alg(
alg_name=target,
arch=args.arch,
max_samples=int(args.evals),
num_workers=int(args.workers),
size=int(args.size),
trials=int(args.trials)
)