Skip to content

Commit ee46030

Browse files
voltjiama-hang
andauthored
Develop ninetoothed.auto_tuner.AutoTuner (#135)
* Add `ninetoothed.auto_tuner.AutoTuner` * Add test cases for `ninetoothed.auto_tuner.AutoTuner` --------- Co-authored-by: MaYuhang <2902139028@qq.com>
1 parent da0a46d commit ee46030

2 files changed

Lines changed: 220 additions & 0 deletions

File tree

src/ninetoothed/auto_tuner.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import hashlib
2+
import json
3+
import os
4+
5+
import triton
6+
7+
from ninetoothed.generation import CACHE_DIR
8+
9+
10+
class AutoTuner:
11+
def __init__(self, funcs, keys):
12+
self._funcs = funcs
13+
14+
self._keys = keys
15+
16+
self._func_to_key = {func: key for func, key in zip(self._funcs, self._keys)}
17+
18+
self._cache_dir = (
19+
_AUTO_TUNING_CACHE_DIR
20+
/ f"{_project_key()}_triton_{triton.__version__.replace('.', '_')}"
21+
)
22+
self._cache_dir.mkdir(parents=True, exist_ok=True)
23+
24+
auto_tuner_key = tuple(self._keys)
25+
cache_key = hashlib.sha256(str(auto_tuner_key).encode("utf-8")).hexdigest()
26+
self._cache_path = self._cache_dir / f"{cache_key}.json"
27+
28+
if self._cache_path.exists():
29+
self._timings = json.loads(self._cache_path.read_text())
30+
else:
31+
self._timings = {key: {} for key in self._keys}
32+
33+
self._best_func = {}
34+
35+
def __call__(self, *args, **kwargs):
36+
if (arg_key := type(self)._make_arg_key(args, kwargs)) in self._best_func:
37+
return self._best_func[arg_key](*args, **kwargs)
38+
39+
timings = self._get_timings(args, kwargs)
40+
41+
best_timing = min(timings)
42+
best_timing_index = timings.index(best_timing)
43+
best_func = self._funcs[best_timing_index]
44+
45+
self._best_func[arg_key] = best_func
46+
47+
return best_func(*args, **kwargs)
48+
49+
def _get_timings(self, args, kwargs):
50+
if (arg_key := type(self)._make_arg_key(args, kwargs)) in self._timings:
51+
return self._timings[arg_key]
52+
53+
timings = [self._get_timing(func, args, kwargs) for func in self._funcs]
54+
55+
self._timings[arg_key] = timings
56+
57+
self._cache_path.write_text(json.dumps(self._timings))
58+
59+
return timings
60+
61+
def _get_timing(self, func, args, kwargs):
62+
func_key = self._func_to_key[func]
63+
64+
data = self._timings[func_key]
65+
66+
if (arg_key := type(self)._make_arg_key(args, kwargs)) in data:
67+
return data[arg_key]
68+
69+
cache_path = self._get_func_cache_path(func)
70+
71+
if cache_path.exists():
72+
data |= json.loads(cache_path.read_text())
73+
74+
if arg_key in data:
75+
return data[arg_key]
76+
77+
timing = triton.testing.do_bench(lambda: func(*args, **kwargs))
78+
79+
data[arg_key] = timing
80+
81+
cache_path.write_text(json.dumps(data))
82+
83+
return timing
84+
85+
def _get_func_cache_path(self, func):
86+
func_key = self._func_to_key[func]
87+
cache_key = hashlib.sha256(str(func_key).encode("utf-8")).hexdigest()
88+
cache_path = self._cache_dir / f"{cache_key}.json"
89+
90+
return cache_path
91+
92+
@staticmethod
93+
def _make_arg_key(args, kwargs):
94+
key_parts = []
95+
96+
def _make_key(arg):
97+
if hasattr(arg, "shape") and hasattr(arg, "dtype"):
98+
return AutoTuner._make_tensor_key(arg)
99+
100+
return str(arg)
101+
102+
for arg in args:
103+
key_parts.append(_make_key(arg))
104+
105+
for key, arg in sorted(kwargs.items()):
106+
key_parts.append(f"{key}={_make_key(arg)}")
107+
108+
arg_key = ", ".join(key_parts)
109+
110+
return arg_key
111+
112+
@staticmethod
113+
def _make_tensor_key(tensor):
114+
return f"tensor(shape={tuple(tensor.shape)}, dtype={str(tensor.dtype).split('.')[-1]})"
115+
116+
117+
_AUTO_TUNING_CACHE_DIR = CACHE_DIR / "auto_tuning"
118+
119+
_FILE_PATH = os.path.abspath(__file__)
120+
121+
_PARENT_DIR = os.path.dirname(_FILE_PATH)
122+
123+
124+
def _project_key():
125+
consolidated_hash = hashlib.sha256()
126+
127+
for dirpath, dirnames, filenames in os.walk(_PARENT_DIR):
128+
dirnames.sort()
129+
filenames.sort()
130+
131+
for filename in filenames:
132+
file_path = os.path.join(dirpath, filename)
133+
134+
if (
135+
not os.path.isfile(file_path)
136+
or os.path.splitext(file_path)[1] == ".pyc"
137+
):
138+
continue
139+
140+
file_hash = _calculate_file_hash(file_path)
141+
consolidated_hash.update(file_hash.encode("utf-8"))
142+
143+
return consolidated_hash.hexdigest()
144+
145+
146+
def _calculate_file_hash(file_path):
147+
with open(file_path, "rb") as f:
148+
return hashlib.sha256(f.read()).hexdigest()

tests/test_auto_tuner.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import time
2+
3+
import pytest
4+
5+
from ninetoothed.auto_tuner import AutoTuner
6+
from tests.utils import get_available_devices
7+
8+
9+
@pytest.mark.parametrize("_", get_available_devices())
10+
@pytest.mark.parametrize("kwargs", ({"a": 2, "b": 4}, {"a": 2, "b": 4, "c": 6, "d": 8}))
11+
@pytest.mark.parametrize("args", ((1,), (1, 3, 5)))
12+
def test_auto_tuner(args, kwargs, _):
13+
auto_tuner = AutoTuner((_foo, _bar), (_foo.__name__, _bar.__name__))
14+
15+
assert not auto_tuner._get_func_cache_path(_foo).exists()
16+
17+
assert not auto_tuner._get_func_cache_path(_bar).exists()
18+
19+
assert not auto_tuner._cache_path.exists()
20+
21+
first_time_start_time = time.perf_counter()
22+
23+
auto_tuner(*args, **kwargs)
24+
25+
first_time_end_time = time.perf_counter()
26+
27+
first_time_elapsed_time = first_time_end_time - first_time_start_time
28+
29+
assert auto_tuner._get_func_cache_path(_foo).exists()
30+
31+
assert auto_tuner._get_func_cache_path(_bar).exists()
32+
33+
assert auto_tuner._cache_path.exists()
34+
35+
second_time_start_time = time.perf_counter()
36+
37+
auto_tuner(*args, **kwargs)
38+
39+
second_time_end_time = time.perf_counter()
40+
41+
second_time_elapsed_time = second_time_end_time - second_time_start_time
42+
43+
assert second_time_elapsed_time < first_time_elapsed_time
44+
45+
auto_tuner._get_func_cache_path(_foo).unlink()
46+
47+
auto_tuner._get_func_cache_path(_bar).unlink()
48+
49+
auto_tuner._cache_path.unlink()
50+
51+
best_func = auto_tuner._best_func[auto_tuner._make_arg_key(args, kwargs)]
52+
53+
if _foo_delay(*args, **kwargs) < _bar_delay(*args, **kwargs):
54+
assert best_func is _foo
55+
else:
56+
assert best_func is _bar
57+
58+
59+
def _foo_delay(*args, **kwargs):
60+
return 0.001 * (2 * len(args) + len(kwargs))
61+
62+
63+
def _bar_delay(*args, **kwargs):
64+
return 0.001 * (len(args) + 2 * len(kwargs))
65+
66+
67+
def _foo(*args, **kwargs):
68+
time.sleep(_foo_delay(*args, **kwargs))
69+
70+
71+
def _bar(*args, **kwargs):
72+
time.sleep(_bar_delay(*args, **kwargs))

0 commit comments

Comments
 (0)