forked from ROCm/aiter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_pretune.py
More file actions
371 lines (322 loc) · 13.3 KB
/
Copy pathtest_pretune.py
File metadata and controls
371 lines (322 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# SPDX-License-Identifier: MIT
# Copyright (C) 2024-2026, Advanced Micro Devices, Inc. All rights reserved.
"""
Unit tests for aiter/utility/pretune.py.
No GPU or torch required — tests exercise file-system and config resolution
logic only. Run with:
python op_tests/test_pretune.py
"""
import json
import logging
import os
import sys
import tempfile
import pandas as pd
REPO_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
CSRC_DIR = os.path.join(REPO_DIR, "csrc")
CFG_PATH = os.path.join(REPO_DIR, "aiter", "jit", "optCompilerConfig.json")
sys.path.insert(0, os.path.join(REPO_DIR, "aiter"))
from utility.pretune import ( # noqa: E402
_all_tune_modules,
_make_untune_csv,
_parse_module_list,
_resolve,
_SCRIPT_FALLBACK,
)
with open(CFG_PATH) as f:
CFG = json.load(f)
logging.getLogger("aiter").addHandler(logging.StreamHandler(sys.stdout))
# ── expected resolution table ────────────────────────────────────────────────
# (tune_module, expected_script_suffix_from_repo_root, expected_config_attr, expect_skip)
EXPECTED = [
(
"module_batched_gemm_a8w8_tune",
"csrc/ck_batched_gemm_a8w8/batched_gemm_a8w8_tune.py",
"AITER_CONFIG_A8W8_BATCHED_GEMM_FILE",
False,
),
(
"module_batched_gemm_bf16_tune",
"csrc/ck_batched_gemm_bf16/batched_gemm_bf16_tune.py",
"AITER_CONFIG_BF16_BATCHED_GEMM_FILE",
False,
),
(
"module_gemm_a4w4_blockscale_tune",
"csrc/ck_gemm_a4w4_blockscale/gemm_a4w4_blockscale_tune.py",
"AITER_CONFIG_GEMM_A4W4_FILE",
False,
),
(
"module_gemm_a8w8_tune",
"csrc/ck_gemm_a8w8/gemm_a8w8_tune.py",
"AITER_CONFIG_GEMM_A8W8_FILE",
False,
),
(
"module_gemm_a8w8_blockscale_tune",
"csrc/ck_gemm_a8w8_blockscale/gemm_a8w8_blockscale_tune.py",
"AITER_CONFIG_GEMM_A8W8_BLOCKSCALE_FILE",
False,
),
(
# cktile variant: falls back to blockscale parent script
"module_gemm_a8w8_blockscale_cktile_tune",
"csrc/ck_gemm_a8w8_blockscale/gemm_a8w8_blockscale_tune.py",
"AITER_CONFIG_GEMM_A8W8_BLOCKSCALE_FILE",
False,
),
(
"module_gemm_a8w8_bpreshuffle_tune",
"csrc/ck_gemm_a8w8_bpreshuffle/gemm_a8w8_bpreshuffle_tune.py",
"AITER_CONFIG_GEMM_A8W8_BPRESHUFFLE_FILE",
False,
),
(
# cktile variant: falls back to bpreshuffle parent script
"module_gemm_a8w8_bpreshuffle_cktile_tune",
"csrc/ck_gemm_a8w8_bpreshuffle/gemm_a8w8_bpreshuffle_tune.py",
"AITER_CONFIG_GEMM_A8W8_BPRESHUFFLE_FILE",
False,
),
(
# no tune script writes to the bpreshuffle blockscale CSV
"module_gemm_a8w8_blockscale_bpreshuffle_tune",
None,
"AITER_CONFIG_GEMM_A8W8_BLOCKSCALE_BPRESHUFFLE_FILE",
True,
),
(
"module_gemm_a8w8_blockscale_bpreshuffle_cktile_tune",
None,
"AITER_CONFIG_GEMM_A8W8_BLOCKSCALE_BPRESHUFFLE_FILE",
True,
),
]
# AITER_CONFIGS tune CSVs with expected shape key columns after _make_untune_csv.
# FMOE and BF16 GEMM are excluded — they have no tune module.
TUNE_CONFIGS = [
("a8w8_blockscale_tuned_gemm.csv", ["M", "N", "K"]),
("a8w8_tuned_gemm.csv", ["M", "N", "K"]),
("a4w4_blockscale_tuned_gemm.csv", ["M", "N", "K"]),
("a8w8_bpreshuffle_tuned_gemm.csv", ["M", "N", "K"]),
("a8w8_blockscale_bpreshuffle_tuned_gemm.csv", ["M", "N", "K"]),
("a8w8_tuned_batched_gemm.csv", ["B", "M", "N", "K"]),
("bf16_tuned_batched_gemm.csv", ["B", "M", "N", "K"]),
]
CONFIGS_DIR = os.path.join(REPO_DIR, "aiter", "configs")
# ── test helpers ──────────────────────────────────────────────────────────────
_passed = 0
_failed = 0
def check(name: str, condition: bool, msg: str = ""):
global _passed, _failed
if condition:
print(f" [PASS] {name}")
_passed += 1
else:
print(f" [FAIL] {name}{': ' + msg if msg else ''}")
_failed += 1
# ── tests ─────────────────────────────────────────────────────────────────────
def test_resolve():
"""_resolve returns correct script path and config attr for every tune module."""
print("\n=== test_resolve ===")
for module, script_suffix, config_attr, expect_skip in EXPECTED:
script, attr = _resolve(module, CFG, CSRC_DIR)
if expect_skip:
check(
f"{module} script=None", script is None, f"expected None, got {script}"
)
else:
expected_abs = os.path.join(REPO_DIR, script_suffix)
check(
f"{module} script path",
script == expected_abs,
f"\n got: {script}\n expected: {expected_abs}",
)
check(
f"{module} script exists",
script is not None and os.path.exists(script),
f"not on disk: {script}",
)
check(
f"{module} config_attr",
attr == config_attr,
f"got {attr!r}, expected {config_attr!r}",
)
def test_all_tune_modules_covered():
"""Every _tune module in optCompilerConfig.json appears in EXPECTED."""
print("\n=== test_all_tune_modules_covered ===")
cfg_modules = set(_all_tune_modules(CFG))
expected_modules = {e[0] for e in EXPECTED}
missing_from_expected = cfg_modules - expected_modules
extra_in_expected = expected_modules - cfg_modules
check(
"no modules missing from EXPECTED",
not missing_from_expected,
f"{missing_from_expected}",
)
check("no extra modules in EXPECTED", not extra_in_expected, f"{extra_in_expected}")
def test_script_fallback_keys_in_config():
"""Every key in _SCRIPT_FALLBACK is a known tune module in optCompilerConfig.json."""
print("\n=== test_script_fallback_keys_in_config ===")
cfg_modules = set(_all_tune_modules(CFG))
unknown = set(_SCRIPT_FALLBACK) - cfg_modules
check(
"all _SCRIPT_FALLBACK keys are valid tune modules",
not unknown,
f"unknown: {unknown}",
)
def test_make_untune_csv():
"""_make_untune_csv retains shape keys, drops metadata columns, deduplicates."""
print("\n=== test_make_untune_csv ===")
for csv_name, expected_shape_keys in TUNE_CONFIGS:
csv_path = os.path.join(CONFIGS_DIR, csv_name)
if not os.path.exists(csv_path):
print(f" [SKIP] {csv_name}: file not present")
continue
orig = pd.read_csv(csv_path)
present_keys = [k for k in expected_shape_keys if k in orig.columns]
expected_unique = orig[present_keys].drop_duplicates().shape[0]
tmp = _make_untune_csv(csv_path, ["B", "M", "N", "K"])
try:
df = pd.read_csv(tmp)
finally:
os.unlink(tmp)
check(
f"{csv_name} columns={expected_shape_keys}",
list(df.columns) == expected_shape_keys,
f"got {df.columns.tolist()}",
)
check(
f"{csv_name} no metadata columns leaked",
not any(
c in df.columns for c in ["gfx", "cu_num", "kernelId", "us", "libtype"]
),
)
check(f"{csv_name} non-empty", len(df) > 0)
check(
f"{csv_name} deduplicated ({expected_unique} unique shapes)",
len(df) == expected_unique,
f"got {len(df)}",
)
def test_make_untune_csv_multi_path():
"""_make_untune_csv handles colon-separated multi-path tune_file correctly."""
print("\n=== test_make_untune_csv_multi_path ===")
rows_a = pd.DataFrame({"M": [128, 256], "N": [512, 512], "K": [1024, 1024]})
rows_b = pd.DataFrame(
{"M": [256, 512], "N": [512, 512], "K": [1024, 1024]}
) # 256 overlaps
with (
tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as fa,
tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as fb,
):
rows_a.to_csv(fa.name, index=False)
rows_b.to_csv(fb.name, index=False)
tune_file = f"{fa.name}{os.pathsep}{fb.name}"
try:
tmp = _make_untune_csv(tune_file, ["B", "M", "N", "K"])
df = pd.read_csv(tmp)
os.unlink(tmp)
# 3 unique (M,N,K): (128,512,1024), (256,512,1024), (512,512,1024)
check("multi-path: 3 unique shapes after dedup", len(df) == 3, f"got {len(df)}")
check("multi-path: columns=[M,N,K]", list(df.columns) == ["M", "N", "K"])
finally:
os.unlink(fa.name)
os.unlink(fb.name)
def test_make_untune_csv_missing_raises():
"""_make_untune_csv raises FileNotFoundError when no CSV path exists."""
print("\n=== test_make_untune_csv_missing_raises ===")
raised = False
try:
_make_untune_csv("/nonexistent/path.csv", ["M", "N", "K"])
except FileNotFoundError:
raised = True
check("raises FileNotFoundError for missing path", raised)
def test_write_tune_file_resolution():
"""In standalone mode, write_tune_file must resolve to the source CSV (in aiter/configs/),
not the ephemeral /tmp merged path.
The logic in run_pretune() strips the _FILE suffix from config_attr to derive a
module-level variable name in core (e.g. AITER_CONFIG_GEMM_A8W8_BLOCKSCALE_FILE →
AITER_CONFIG_GEMM_A8W8_BLOCKSCALE), then calls getattr(core, source_attr).
This test verifies that all supported modules have a matching module-level variable
in core.py whose value points inside aiter/configs/ (not /tmp/).
"""
print("\n=== test_write_tune_file_resolution ===")
core_py = os.path.join(REPO_DIR, "aiter", "jit", "core.py")
with open(core_py, encoding="utf-8") as f:
core_src = f.read()
for module, _, config_attr, expect_skip in EXPECTED:
if expect_skip or config_attr is None:
continue
source_attr = config_attr.removesuffix("_FILE")
if source_attr == config_attr:
check(
f"{module} config_attr has _FILE suffix",
False,
f"config_attr {config_attr!r} has no _FILE suffix — cannot derive source_attr",
)
continue
# Verify the module-level variable exists in core.py
check(
f"{module} source_attr '{source_attr}' defined in core.py",
f"{source_attr} " in core_src or f"{source_attr}=" in core_src,
f"not found in core.py — getattr(core, {source_attr!r}) would return None",
)
# Verify it maps to aiter/configs/ (not /tmp/)
import re
m = re.search(
rf'{source_attr}\s*=\s*os\.getenv\(\s*["\'][\w]+["\'],\s*([^\)]+)\)',
core_src,
)
if m:
default_val = m.group(1).strip().strip('"').strip("'")
check(
f"{module} default write_tune_file is in aiter/configs/",
"aiter/configs" in default_val or "aiter_meta/configs" in default_val,
f"default points to: {default_val!r}",
)
def test_parse_pretune_modules():
"""PRETUNE_MODULES env values parse to the correct module lists."""
print("\n=== test_parse_pretune_modules ===")
cases = [
(
"module_gemm_a8w8_blockscale_tune",
["module_gemm_a8w8_blockscale_tune"],
),
(
"module_gemm_a8w8_tune,module_gemm_a8w8_blockscale_tune",
["module_gemm_a8w8_tune", "module_gemm_a8w8_blockscale_tune"],
),
(
" module_gemm_a8w8_tune , module_gemm_a8w8_blockscale_tune ",
["module_gemm_a8w8_tune", "module_gemm_a8w8_blockscale_tune"],
),
]
for env_value, expected in cases:
modules = _parse_module_list(env_value, CFG)
check(f"parse {env_value.strip()!r}", modules == expected, f"got {modules}")
# "all" expands to every _tune module, excluding _unsupported entries
all_modules = _all_tune_modules(CFG)
check("all: count matches EXPECTED", len(all_modules) == len(EXPECTED))
check("all: set matches EXPECTED", set(all_modules) == {e[0] for e in EXPECTED})
_unsupported = {m for m, v in _SCRIPT_FALLBACK.items() if v is None}
all_parsed = _parse_module_list("all", CFG)
check(
"all: _unsupported modules excluded",
not any(m in _unsupported for m in all_parsed),
f"unsupported in result: {[m for m in all_parsed if m in _unsupported]}",
)
# ── main ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
test_resolve()
test_all_tune_modules_covered()
test_script_fallback_keys_in_config()
test_make_untune_csv()
test_make_untune_csv_multi_path()
test_make_untune_csv_missing_raises()
test_write_tune_file_resolution()
test_parse_pretune_modules()
print(f"\n{'='*50}")
print(f"Results: {_passed} passed, {_failed} failed")
if _failed:
sys.exit(1)