pelagos-decoder 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pelagos/__init__.py +34 -0
- pelagos/_process_worker.py +56 -0
- pelagos/adapter.py +858 -0
- pelagos/autotune.py +793 -0
- pelagos/decoder.py +3891 -0
- pelagos/graph.py +479 -0
- pelagos/kernels.py +2208 -0
- pelagos/process.py +287 -0
- pelagos/stim/__init__.py +11 -0
- pelagos/stim/sinter/__init__.py +11 -0
- pelagos/stim/sinter/check_matrices.py +112 -0
- pelagos/utils.py +995 -0
- pelagos_decoder-0.1.0.dist-info/METADATA +317 -0
- pelagos_decoder-0.1.0.dist-info/RECORD +16 -0
- pelagos_decoder-0.1.0.dist-info/WHEEL +4 -0
- pelagos_decoder-0.1.0.dist-info/licenses/LICENSE +201 -0
pelagos/autotune.py
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
# (C) Copyright IBM 2026
|
|
2
|
+
#
|
|
3
|
+
# This code is licensed under the Apache License, Version 2.0. You may
|
|
4
|
+
# obtain a copy of this license in the LICENSE file in the root directory
|
|
5
|
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
#
|
|
7
|
+
# Any modifications or derivative works of this code must retain this
|
|
8
|
+
# copyright notice, and modified files need to carry a notice indicating
|
|
9
|
+
# that they have been altered from the originals.
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import json
|
|
13
|
+
import hashlib
|
|
14
|
+
import platform
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple, Callable
|
|
18
|
+
import itertools, random
|
|
19
|
+
import threading
|
|
20
|
+
import triton
|
|
21
|
+
import torch
|
|
22
|
+
import csv
|
|
23
|
+
|
|
24
|
+
DEFAULT_CACHE = Path(os.getenv("RELAY_TUNE_CACHE", "~/.cache/relay_bp_tune.json")).expanduser()
|
|
25
|
+
|
|
26
|
+
# Global lock to make cache writes thread-safe across decoder threads
|
|
27
|
+
_CACHE_LOCK = threading.Lock()
|
|
28
|
+
|
|
29
|
+
# Optional CSV logging (set RELAY_TUNE_LOG_CSV to enable)
|
|
30
|
+
RUN_ID = os.getenv("RELAY_TUNE_RUN_ID", datetime.now(timezone.utc).isoformat())
|
|
31
|
+
LOG_CSV_PATH = os.getenv("RELAY_TUNE_LOG_CSV", "./autotuner_" + RUN_ID + ".csv").strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _csv_log_enabled() -> bool:
|
|
35
|
+
return bool(LOG_CSV_PATH)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _safe_str(x):
|
|
39
|
+
try:
|
|
40
|
+
return json.dumps(x, sort_keys=True)
|
|
41
|
+
except Exception:
|
|
42
|
+
return str(x)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _append_csv_row(row: dict):
|
|
46
|
+
if not _csv_log_enabled():
|
|
47
|
+
return
|
|
48
|
+
path = Path(LOG_CSV_PATH).expanduser()
|
|
49
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
write_header = (not path.exists()) or (path.exists() and path.stat().st_size == 0)
|
|
51
|
+
fieldnames = [
|
|
52
|
+
"timestamp",
|
|
53
|
+
"run_id",
|
|
54
|
+
"kernel",
|
|
55
|
+
"backend",
|
|
56
|
+
"device_name",
|
|
57
|
+
"device_index",
|
|
58
|
+
"sm",
|
|
59
|
+
"torch",
|
|
60
|
+
"triton",
|
|
61
|
+
"rep",
|
|
62
|
+
"trial_idx",
|
|
63
|
+
"trial_total",
|
|
64
|
+
"grid",
|
|
65
|
+
"BLOCK_SIZE",
|
|
66
|
+
"ROWS_PER_CHK",
|
|
67
|
+
"ROWS_PER_VAR",
|
|
68
|
+
"BTILE",
|
|
69
|
+
"BLOCK_C",
|
|
70
|
+
"BLOCK_V",
|
|
71
|
+
"num_warps",
|
|
72
|
+
"num_stages",
|
|
73
|
+
"time_s",
|
|
74
|
+
"throughput_units_per_s",
|
|
75
|
+
"work_units",
|
|
76
|
+
"best_so_far",
|
|
77
|
+
"problem_key",
|
|
78
|
+
"meta_base",
|
|
79
|
+
]
|
|
80
|
+
with path.open("a", newline="") as f:
|
|
81
|
+
w = csv.DictWriter(f, fieldnames=fieldnames)
|
|
82
|
+
if write_header:
|
|
83
|
+
w.writeheader()
|
|
84
|
+
w.writerow({k: row.get(k, "") for k in fieldnames})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _fingerprint() -> Dict[str, Any]:
|
|
88
|
+
backend = "cpu"
|
|
89
|
+
device_info: Dict[str, Any] = {}
|
|
90
|
+
if torch.cuda.is_available():
|
|
91
|
+
try:
|
|
92
|
+
dev = torch.cuda.current_device()
|
|
93
|
+
props = torch.cuda.get_device_properties(dev)
|
|
94
|
+
major = getattr(props, "major", 0)
|
|
95
|
+
minor = getattr(props, "minor", 0)
|
|
96
|
+
sm = major * 10 + minor
|
|
97
|
+
device_info = {
|
|
98
|
+
"device_name": getattr(props, "name", "unknown"),
|
|
99
|
+
"capability": (major, minor),
|
|
100
|
+
"sm": sm,
|
|
101
|
+
"total_mem": int(getattr(props, "total_memory", 0)),
|
|
102
|
+
"multi_processor_count": int(getattr(props, "multi_processor_count", 0)),
|
|
103
|
+
}
|
|
104
|
+
backend = "cuda" if torch.version.cuda is not None else "rocm"
|
|
105
|
+
except Exception:
|
|
106
|
+
backend = (
|
|
107
|
+
"cuda"
|
|
108
|
+
if torch.version.cuda is not None
|
|
109
|
+
else ("rocm" if getattr(torch.version, "hip", None) else "gpu")
|
|
110
|
+
)
|
|
111
|
+
dev = torch.cuda.current_device()
|
|
112
|
+
device_info = {
|
|
113
|
+
"device_name": torch.cuda.get_device_name(dev),
|
|
114
|
+
}
|
|
115
|
+
else:
|
|
116
|
+
device_info = {"device_name": "cpu"}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"backend": backend,
|
|
120
|
+
**device_info,
|
|
121
|
+
"driver_cuda": torch.version.cuda,
|
|
122
|
+
"driver_hip": getattr(torch.version, "hip", None),
|
|
123
|
+
"torch": torch.__version__,
|
|
124
|
+
"triton": triton.__version__,
|
|
125
|
+
"python": platform.python_version(),
|
|
126
|
+
"arch_override": os.getenv("SM", ""),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _make_cache_key(kernel_name: str, problem_key: Dict[str, Any]) -> str:
|
|
131
|
+
fp = _fingerprint()
|
|
132
|
+
blob = json.dumps({"kernel": kernel_name, "fp": fp, "shape": problem_key}, sort_keys=True)
|
|
133
|
+
return hashlib.blake2b(blob.encode(), digest_size=24).hexdigest()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def load_cache() -> Dict[str, Dict[str, Any]]:
|
|
137
|
+
"""Load cache file with timeout/retry to prevent blocking on distributed filesystems."""
|
|
138
|
+
import time
|
|
139
|
+
|
|
140
|
+
max_retries = 5
|
|
141
|
+
retry_delay = 0.05 # 50ms
|
|
142
|
+
|
|
143
|
+
if not DEFAULT_CACHE.exists():
|
|
144
|
+
return {}
|
|
145
|
+
|
|
146
|
+
for attempt in range(max_retries):
|
|
147
|
+
try:
|
|
148
|
+
return json.loads(DEFAULT_CACHE.read_text())
|
|
149
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
150
|
+
# File might be in the middle of being written/renamed
|
|
151
|
+
if attempt < max_retries - 1:
|
|
152
|
+
time.sleep(retry_delay)
|
|
153
|
+
continue
|
|
154
|
+
# Last attempt failed, return empty dict (will trigger autotuning)
|
|
155
|
+
return {}
|
|
156
|
+
except Exception:
|
|
157
|
+
# Other unexpected errors, return empty dict
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def save_cache(table: Dict[str, Dict[str, Any]]):
|
|
164
|
+
"""Save cache using atomic file operations (temp file + rename).
|
|
165
|
+
|
|
166
|
+
Uses atomic write (temp file + rename) with retry logic to handle concurrent
|
|
167
|
+
writes from multiple processes. Temp file includes PID for uniqueness per process.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
table: Cache data to save
|
|
171
|
+
"""
|
|
172
|
+
import time
|
|
173
|
+
import os
|
|
174
|
+
|
|
175
|
+
max_retries = 10
|
|
176
|
+
retry_delay = 0.1 # seconds
|
|
177
|
+
|
|
178
|
+
# Ensure directory exists
|
|
179
|
+
DEFAULT_CACHE.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
# Use PID in temp filename to make it unique per process
|
|
182
|
+
tmp = DEFAULT_CACHE.parent / f"{DEFAULT_CACHE.stem}.tmp.{os.getpid()}"
|
|
183
|
+
cache_data = json.dumps(table, indent=2, sort_keys=True)
|
|
184
|
+
|
|
185
|
+
for attempt in range(max_retries):
|
|
186
|
+
try:
|
|
187
|
+
# Use atomic write: write to temp file, then rename
|
|
188
|
+
# This is safe even with concurrent writes (last write wins, which is fine)
|
|
189
|
+
tmp.write_text(cache_data)
|
|
190
|
+
if not tmp.exists():
|
|
191
|
+
raise FileNotFoundError(f"Temp file {tmp} was not created")
|
|
192
|
+
# Atomic rename - works on most filesystems and is safe for concurrent access
|
|
193
|
+
tmp.replace(DEFAULT_CACHE)
|
|
194
|
+
return # Success
|
|
195
|
+
except (OSError, FileNotFoundError) as e:
|
|
196
|
+
if attempt < max_retries - 1:
|
|
197
|
+
# Wait with exponential backoff before retry
|
|
198
|
+
time.sleep(retry_delay * (2 ** min(attempt, 4))) # Cap exponential growth
|
|
199
|
+
# Clean up any stale temp file from previous attempt
|
|
200
|
+
if tmp.exists():
|
|
201
|
+
try:
|
|
202
|
+
tmp.unlink()
|
|
203
|
+
except OSError:
|
|
204
|
+
pass
|
|
205
|
+
continue
|
|
206
|
+
else:
|
|
207
|
+
# Last attempt failed, log and re-raise
|
|
208
|
+
import warnings
|
|
209
|
+
|
|
210
|
+
warnings.warn(f"Failed to save autotune cache after {max_retries} attempts: {e}")
|
|
211
|
+
raise
|
|
212
|
+
finally:
|
|
213
|
+
# Clean up temp file only if rename failed (file still exists)
|
|
214
|
+
# If rename succeeded, tmp no longer exists (it became DEFAULT_CACHE)
|
|
215
|
+
if tmp.exists():
|
|
216
|
+
try:
|
|
217
|
+
tmp.unlink()
|
|
218
|
+
except OSError:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def try_get_saved(kernel_name: str, problem_key: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
223
|
+
"""Check if cached tuning results exist for a kernel/problem combination.
|
|
224
|
+
|
|
225
|
+
Reads cache based on atomic file operations and accept that duplicate autotuning
|
|
226
|
+
might occasionally occur.
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
table = load_cache()
|
|
230
|
+
key = _make_cache_key(kernel_name, problem_key)
|
|
231
|
+
entry = table.get(key)
|
|
232
|
+
if entry is None:
|
|
233
|
+
return None
|
|
234
|
+
return entry["selected"]
|
|
235
|
+
except Exception:
|
|
236
|
+
# Any error reading cache, return None (will trigger autotuning)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def set_saved(kernel_name: str, problem_key: Dict[str, Any], meta: Dict[str, Any]):
|
|
241
|
+
"""Save tuned meta for a kernel/problem combination.
|
|
242
|
+
|
|
243
|
+
This function is called from multiple decoder processes (one per GPU) during
|
|
244
|
+
autotuning. We use atomic file operations (temp file + rename).
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
table = load_cache()
|
|
248
|
+
key = _make_cache_key(kernel_name, problem_key)
|
|
249
|
+
record = {
|
|
250
|
+
"kernel": str(kernel_name),
|
|
251
|
+
"problem": problem_key,
|
|
252
|
+
"selected": meta,
|
|
253
|
+
"fingerprint": _fingerprint(),
|
|
254
|
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
|
255
|
+
"format": 2,
|
|
256
|
+
}
|
|
257
|
+
table[key] = record
|
|
258
|
+
# Save - atomic operations handle concurrency
|
|
259
|
+
save_cache(table)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _is_invalid_config_error(e: Exception) -> bool:
|
|
263
|
+
"""Check if an exception indicates an invalid configuration error (common on AMD HIP).
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
e: Exception to check
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
True if the exception indicates an invalid configuration error
|
|
270
|
+
"""
|
|
271
|
+
error_msg = str(e).lower()
|
|
272
|
+
error_repr = repr(e).lower()
|
|
273
|
+
# Check the exception itself
|
|
274
|
+
is_invalid_config = (
|
|
275
|
+
"invalid configuration" in error_msg
|
|
276
|
+
or "hip" in error_msg
|
|
277
|
+
or "code: 9" in error_msg
|
|
278
|
+
or "invalid configuration" in error_repr
|
|
279
|
+
or "hip" in error_repr
|
|
280
|
+
or "code: 9" in error_repr
|
|
281
|
+
)
|
|
282
|
+
# Also check the exception's cause/context (for wrapped exceptions)
|
|
283
|
+
# Python uses __cause__ for explicit chaining, __context__ for implicit chaining
|
|
284
|
+
if not is_invalid_config:
|
|
285
|
+
for exc in [e.__cause__, e.__context__]:
|
|
286
|
+
if exc is not None:
|
|
287
|
+
cause_msg = str(exc).lower()
|
|
288
|
+
cause_repr = repr(exc).lower()
|
|
289
|
+
if (
|
|
290
|
+
"invalid configuration" in cause_msg
|
|
291
|
+
or "hip" in cause_msg
|
|
292
|
+
or "code: 9" in cause_msg
|
|
293
|
+
or "invalid configuration" in cause_repr
|
|
294
|
+
or "hip" in cause_repr
|
|
295
|
+
or "code: 9" in cause_repr
|
|
296
|
+
):
|
|
297
|
+
is_invalid_config = True
|
|
298
|
+
break
|
|
299
|
+
return is_invalid_config
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def bench_and_select(
|
|
303
|
+
kernel,
|
|
304
|
+
grid,
|
|
305
|
+
args: Tuple,
|
|
306
|
+
meta_base: Dict[str, Any],
|
|
307
|
+
configs: List[Dict[str, Any]],
|
|
308
|
+
number: int = 20,
|
|
309
|
+
grid_fn: Optional[Callable[[Dict[str, Any]], Tuple[int, ...]]] = None,
|
|
310
|
+
) -> Dict[str, Any]:
|
|
311
|
+
"""Benchmark kernel configurations and select the best.
|
|
312
|
+
|
|
313
|
+
This function minimizes raw wall time, which is fair because:
|
|
314
|
+
- grid_fn adjusts grid size to keep total work constant (e.g., BTILE doubles -> grid dimension halves)
|
|
315
|
+
- For BTILE kernels: grid = (C, (B + bt - 1) // bt), so total work = B * C is constant
|
|
316
|
+
- For ROWS_PER_CHK kernels: grid = ((B*C + rows - 1) // rows,), so total work = B * C is constant
|
|
317
|
+
|
|
318
|
+
We log normalized throughput (work_units/time) for analysis, but selection uses wall time
|
|
319
|
+
since work is constant across configs.
|
|
320
|
+
"""
|
|
321
|
+
verbose = os.getenv("RELAY_TUNE_VERBOSE", "0") == "1"
|
|
322
|
+
# Timeout per config: skip if single iteration takes > 5 seconds (likely hanging/compilation issue)
|
|
323
|
+
timeout_per_iter_ms = float(os.getenv("RELAY_TUNE_TIMEOUT_MS", "5000.0"))
|
|
324
|
+
|
|
325
|
+
# Detect backend for grid size limits (AMD HIP has stricter limits)
|
|
326
|
+
fp = _fingerprint()
|
|
327
|
+
is_amd = fp.get("backend") == "rocm" or fp.get("driver_hip") is not None
|
|
328
|
+
# AMD GPUs may have practical grid size limits lower than theoretical max
|
|
329
|
+
# We use a conservative limit to avoid "invalid configuration" errors
|
|
330
|
+
max_grid_dim = 2**24 if is_amd else 2**31 - 1 # 16M for AMD, 2B for NVIDIA
|
|
331
|
+
|
|
332
|
+
best_t = float("inf")
|
|
333
|
+
best_cfg: Optional[Dict[str, Any]] = None
|
|
334
|
+
total = len(configs)
|
|
335
|
+
kname = getattr(kernel, "__name__", "kernel")
|
|
336
|
+
if verbose:
|
|
337
|
+
import threading
|
|
338
|
+
|
|
339
|
+
thread_name = threading.current_thread().name
|
|
340
|
+
try:
|
|
341
|
+
dev = torch.cuda.current_device()
|
|
342
|
+
except Exception:
|
|
343
|
+
dev = None
|
|
344
|
+
dev_str = f", gpu={dev}" if dev is not None else ""
|
|
345
|
+
backend_str = " (AMD)" if is_amd else ""
|
|
346
|
+
print(
|
|
347
|
+
f"[relay-bp-triton][thread={thread_name}{dev_str}] Tuning {kname} with {total} configs, reps={number}{backend_str}"
|
|
348
|
+
)
|
|
349
|
+
for idx, cfg in enumerate(configs, start=1):
|
|
350
|
+
conf_kwargs = {k: v for k, v in cfg.items() if k.isupper()}
|
|
351
|
+
num_warps = cfg.get("num_warps")
|
|
352
|
+
num_stages = cfg.get("num_stages")
|
|
353
|
+
grid_local = grid_fn(cfg) if grid_fn is not None else grid
|
|
354
|
+
|
|
355
|
+
# Check for invalid grid sizes (e.g., too large for AMD HIP backend)
|
|
356
|
+
# AMD GPUs have stricter practical limits than NVIDIA
|
|
357
|
+
if any(dim > max_grid_dim for dim in grid_local):
|
|
358
|
+
if verbose:
|
|
359
|
+
print(
|
|
360
|
+
f" [{idx}/{total}] SKIP: grid size {grid_local} exceeds maximum grid dimension {max_grid_dim}"
|
|
361
|
+
)
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Try to launch kernel - catch invalid configuration errors (e.g., AMD HIP limits)
|
|
365
|
+
# Note: AMD HIP errors can manifest as RuntimeError or SystemError (wrapped)
|
|
366
|
+
try:
|
|
367
|
+
kernel[grid_local](
|
|
368
|
+
*args, **meta_base, **conf_kwargs, num_warps=num_warps, num_stages=num_stages
|
|
369
|
+
)
|
|
370
|
+
torch.cuda.synchronize()
|
|
371
|
+
for _ in range(2):
|
|
372
|
+
kernel[grid_local](
|
|
373
|
+
*args, **meta_base, **conf_kwargs, num_warps=num_warps, num_stages=num_stages
|
|
374
|
+
)
|
|
375
|
+
torch.cuda.synchronize()
|
|
376
|
+
except (RuntimeError, SystemError) as e:
|
|
377
|
+
# Primary handler for expected exception types
|
|
378
|
+
# Skip configurations that fail with invalid configuration errors (common on AMD HIP)
|
|
379
|
+
if _is_invalid_config_error(e):
|
|
380
|
+
if verbose:
|
|
381
|
+
print(
|
|
382
|
+
f" [{idx}/{total}] SKIP: invalid configuration for grid={grid_local}, warps={num_warps}, stages={num_stages}: {e}"
|
|
383
|
+
)
|
|
384
|
+
continue
|
|
385
|
+
# Re-raise other errors (unexpected errors)
|
|
386
|
+
raise
|
|
387
|
+
except Exception as e:
|
|
388
|
+
# Defensive catch-all: check if it's an invalid config error in any form
|
|
389
|
+
if _is_invalid_config_error(e):
|
|
390
|
+
if verbose:
|
|
391
|
+
print(
|
|
392
|
+
f" [{idx}/{total}] SKIP: invalid configuration (caught as {type(e).__name__}) for grid={grid_local}, warps={num_warps}, stages={num_stages}: {e}"
|
|
393
|
+
)
|
|
394
|
+
continue
|
|
395
|
+
# Re-raise unexpected errors
|
|
396
|
+
raise
|
|
397
|
+
|
|
398
|
+
def run():
|
|
399
|
+
kernel[grid_local](
|
|
400
|
+
*args, **meta_base, **conf_kwargs, num_warps=num_warps, num_stages=num_stages
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Benchmark with a fixed number of repetitions (iterations), not a time budget
|
|
404
|
+
iters = max(1, int(number))
|
|
405
|
+
times_ms = []
|
|
406
|
+
skipped_due_to_timeout = False
|
|
407
|
+
skipped_due_to_error = False
|
|
408
|
+
for iter_idx in range(iters):
|
|
409
|
+
try:
|
|
410
|
+
start_evt = torch.cuda.Event(enable_timing=True)
|
|
411
|
+
end_evt = torch.cuda.Event(enable_timing=True)
|
|
412
|
+
start_evt.record()
|
|
413
|
+
run()
|
|
414
|
+
end_evt.record()
|
|
415
|
+
torch.cuda.synchronize()
|
|
416
|
+
iter_time = start_evt.elapsed_time(end_evt)
|
|
417
|
+
# Skip configs that take too long per iteration (likely register spilling/hanging)
|
|
418
|
+
if iter_time > timeout_per_iter_ms:
|
|
419
|
+
if verbose:
|
|
420
|
+
print(
|
|
421
|
+
f" [{idx}/{total}] SKIP: iteration {iter_idx + 1} took {iter_time:.1f}ms > {timeout_per_iter_ms:.1f}ms timeout"
|
|
422
|
+
)
|
|
423
|
+
skipped_due_to_timeout = True
|
|
424
|
+
break
|
|
425
|
+
times_ms.append(iter_time)
|
|
426
|
+
except (RuntimeError, SystemError) as e:
|
|
427
|
+
# Catch runtime/system errors during benchmarking (e.g., invalid configuration discovered during run)
|
|
428
|
+
if _is_invalid_config_error(e):
|
|
429
|
+
if verbose:
|
|
430
|
+
print(
|
|
431
|
+
f" [{idx}/{total}] SKIP: invalid configuration during benchmark: {e}"
|
|
432
|
+
)
|
|
433
|
+
skipped_due_to_error = True
|
|
434
|
+
break
|
|
435
|
+
# Re-raise other errors (unexpected errors)
|
|
436
|
+
raise
|
|
437
|
+
except Exception as e:
|
|
438
|
+
# Defensive catch-all: check if it's an invalid config error in any form
|
|
439
|
+
if _is_invalid_config_error(e):
|
|
440
|
+
if verbose:
|
|
441
|
+
print(
|
|
442
|
+
f" [{idx}/{total}] SKIP: invalid configuration during benchmark (caught as {type(e).__name__}): {e}"
|
|
443
|
+
)
|
|
444
|
+
skipped_due_to_error = True
|
|
445
|
+
break
|
|
446
|
+
# Re-raise unexpected errors
|
|
447
|
+
raise
|
|
448
|
+
|
|
449
|
+
if skipped_due_to_timeout or skipped_due_to_error:
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
if not times_ms:
|
|
453
|
+
if verbose:
|
|
454
|
+
print(f" [{idx}/{total}] SKIP: no valid timings")
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
t = sum(times_ms) / len(times_ms)
|
|
458
|
+
|
|
459
|
+
# Compute normalized throughput metrics for analysis
|
|
460
|
+
# Note: grid_fn adjusts grid size to keep total work constant (e.g., BTILE doubles -> grid halves),
|
|
461
|
+
# so raw wall time comparison is valid. But we log normalized metrics for visibility.
|
|
462
|
+
total_programs = 1
|
|
463
|
+
for dim in grid_local:
|
|
464
|
+
total_programs *= dim
|
|
465
|
+
# Estimate work: B*C for C2V, B*V for V2C, B*E for message ops
|
|
466
|
+
# Use meta_base values if available, otherwise estimate from grid
|
|
467
|
+
B = meta_base.get("B", 1)
|
|
468
|
+
C = meta_base.get("C", grid_local[0] if len(grid_local) > 0 else 1)
|
|
469
|
+
V = meta_base.get("V", grid_local[0] if len(grid_local) > 0 else 1)
|
|
470
|
+
E = meta_base.get("E", 1)
|
|
471
|
+
# Approximate work units (kernels process different amounts, but rough estimate)
|
|
472
|
+
kl = kname.lower()
|
|
473
|
+
if "c2v" in kl:
|
|
474
|
+
work_units = B * C # one check per batch item
|
|
475
|
+
elif "v2c" in kl:
|
|
476
|
+
work_units = B * V # one variable per batch item
|
|
477
|
+
elif "parity" in kl:
|
|
478
|
+
work_units = B * C # one check per batch item
|
|
479
|
+
elif "check_and_select" in kl:
|
|
480
|
+
work_units = B * C # validates checks and selects per lane
|
|
481
|
+
else:
|
|
482
|
+
work_units = B * E # fallback: edges processed
|
|
483
|
+
throughput = work_units / t if t > 0 else 0.0
|
|
484
|
+
|
|
485
|
+
if verbose:
|
|
486
|
+
cfg_str = ", ".join([f"{k}={v}" for k, v in conf_kwargs.items()])
|
|
487
|
+
print(
|
|
488
|
+
f" [{idx}/{total}] {cfg_str}, warps={num_warps}, stages={num_stages} -> {t:.3e} s (throughput: {throughput:.1e} units/s)"
|
|
489
|
+
)
|
|
490
|
+
_append_csv_row(
|
|
491
|
+
{
|
|
492
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
493
|
+
"run_id": RUN_ID,
|
|
494
|
+
"kernel": kname,
|
|
495
|
+
"backend": fp.get("backend"),
|
|
496
|
+
"device_name": fp.get("device_name"),
|
|
497
|
+
"device_index": fp.get("device_index"),
|
|
498
|
+
"sm": fp.get("sm"),
|
|
499
|
+
"torch": fp.get("torch"),
|
|
500
|
+
"triton": fp.get("triton"),
|
|
501
|
+
"rep": number,
|
|
502
|
+
"trial_idx": idx,
|
|
503
|
+
"trial_total": total,
|
|
504
|
+
"grid": _safe_str(grid_local),
|
|
505
|
+
"BLOCK_SIZE": conf_kwargs.get("BLOCK_SIZE"),
|
|
506
|
+
"ROWS_PER_CHK": conf_kwargs.get("ROWS_PER_CHK"),
|
|
507
|
+
"ROWS_PER_VAR": conf_kwargs.get("ROWS_PER_VAR"),
|
|
508
|
+
"BTILE": conf_kwargs.get("BTILE"),
|
|
509
|
+
"BLOCK_C": conf_kwargs.get("BLOCK_C"),
|
|
510
|
+
"BLOCK_V": conf_kwargs.get("BLOCK_V"),
|
|
511
|
+
"num_warps": num_warps,
|
|
512
|
+
"num_stages": num_stages,
|
|
513
|
+
"time_s": t,
|
|
514
|
+
"throughput_units_per_s": throughput,
|
|
515
|
+
"work_units": work_units,
|
|
516
|
+
"best_so_far": (t < best_t),
|
|
517
|
+
"problem_key": _safe_str({}),
|
|
518
|
+
"meta_base": _safe_str(meta_base),
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
if t < best_t:
|
|
522
|
+
best_t = t
|
|
523
|
+
best_cfg = {"num_warps": num_warps, "num_stages": num_stages, **conf_kwargs}
|
|
524
|
+
if verbose:
|
|
525
|
+
print(f" -> new best")
|
|
526
|
+
if best_cfg is None:
|
|
527
|
+
raise RuntimeError("No config benchmarked")
|
|
528
|
+
if verbose:
|
|
529
|
+
import threading
|
|
530
|
+
|
|
531
|
+
thread_name = threading.current_thread().name
|
|
532
|
+
try:
|
|
533
|
+
dev = torch.cuda.current_device()
|
|
534
|
+
except Exception:
|
|
535
|
+
dev = None
|
|
536
|
+
dev_str = f", gpu={dev}" if dev is not None else ""
|
|
537
|
+
best_str = ", ".join([f"{k}={v}" for k, v in best_cfg.items()])
|
|
538
|
+
# Include simple key summary to avoid misleading cross-run comparisons
|
|
539
|
+
key_str = f"B={meta_base.get('B')}, C={meta_base.get('C')}, V={meta_base.get('V')}"
|
|
540
|
+
print(f"[relay-bp-triton][thread={thread_name}{dev_str}] Selected: {best_str} [{key_str}]")
|
|
541
|
+
return best_cfg
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# ---- Dynamic config builders for bench path ----
|
|
545
|
+
def _parse_list(env: str, default: list[int]) -> list[int]:
|
|
546
|
+
v = os.getenv(env)
|
|
547
|
+
if v is None:
|
|
548
|
+
return default
|
|
549
|
+
try:
|
|
550
|
+
return [int(x) for x in v.replace(" ", "").split(",") if x]
|
|
551
|
+
except Exception:
|
|
552
|
+
return default
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _cap(n: int | None = None, env: str = "RELAY_TUNE_MAX_CFG", default: int = 512) -> int:
|
|
556
|
+
try:
|
|
557
|
+
return int(os.getenv(env, default if n is None else n))
|
|
558
|
+
except Exception:
|
|
559
|
+
return default if n is None else n
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _sample(
|
|
563
|
+
cfgs: list[triton.Config],
|
|
564
|
+
env: str = "RELAY_TUNE_SAMPLE",
|
|
565
|
+
default: int | None = None,
|
|
566
|
+
seed: int = 0,
|
|
567
|
+
) -> list[triton.Config]:
|
|
568
|
+
k = os.getenv(env)
|
|
569
|
+
if k is None and default is None:
|
|
570
|
+
return cfgs
|
|
571
|
+
try:
|
|
572
|
+
kint = int(k if k is not None else default)
|
|
573
|
+
except Exception:
|
|
574
|
+
return cfgs
|
|
575
|
+
if kint and kint < len(cfgs):
|
|
576
|
+
rng = random.Random(int(os.getenv("RELAY_TUNE_SEED", seed)))
|
|
577
|
+
return rng.sample(cfgs, kint)
|
|
578
|
+
return cfgs
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _make_configs(space: dict[str, list[int]], cap: int | None = None) -> list[triton.Config]:
|
|
582
|
+
keys = list(space.keys())
|
|
583
|
+
vals = [space[k] for k in keys]
|
|
584
|
+
out: list[triton.Config] = []
|
|
585
|
+
for combo in itertools.product(*vals):
|
|
586
|
+
kw = dict(zip(keys, combo))
|
|
587
|
+
bs = kw.get("BLOCK_SIZE", 0)
|
|
588
|
+
warps = kw.get("num_warps", 0)
|
|
589
|
+
out.append(
|
|
590
|
+
triton.Config(kw, num_warps=kw.get("num_warps", 2), num_stages=kw.get("num_stages", 2))
|
|
591
|
+
)
|
|
592
|
+
out = out[: _cap(cap)]
|
|
593
|
+
out = _sample(out)
|
|
594
|
+
return out
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def build_c2v_configs() -> list[triton.Config]:
|
|
598
|
+
bs = _parse_list("RELAY_SWEEP_C2V_BLOCK", [1, 2, 4, 8, 16])
|
|
599
|
+
r = _parse_list("RELAY_SWEEP_ROWS_PER_CHK", [1, 2, 4, 8])
|
|
600
|
+
wp = _parse_list("RELAY_SWEEP_C2V_WARPS", [1, 2, 4])
|
|
601
|
+
st = _parse_list("RELAY_SWEEP_C2V_STAGES", [1, 2])
|
|
602
|
+
space = {"BLOCK_SIZE": bs, "ROWS_PER_CHK": r, "num_warps": wp, "num_stages": st}
|
|
603
|
+
return _make_configs(space)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def build_v2c_configs() -> list[triton.Config]:
|
|
607
|
+
bs = _parse_list("RELAY_SWEEP_V2C_BLOCK", [1, 2, 4, 8, 16, 32])
|
|
608
|
+
r = _parse_list("RELAY_SWEEP_ROWS_PER_VAR", [1, 2, 4, 8])
|
|
609
|
+
wp = _parse_list("RELAY_SWEEP_V2C_WARPS", [1, 2, 4])
|
|
610
|
+
st = _parse_list("RELAY_SWEEP_V2C_STAGES", [1, 2])
|
|
611
|
+
space = {"BLOCK_SIZE": bs, "ROWS_PER_VAR": r, "num_warps": wp, "num_stages": st}
|
|
612
|
+
return _make_configs(space)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def build_c2v_btile_configs() -> list[triton.Config]:
|
|
616
|
+
"""Configs for C2V BTILE kernels (check-major, dimension ~C).
|
|
617
|
+
|
|
618
|
+
Environment overrides (C2V-specific):
|
|
619
|
+
- RELAY_SWEEP_C2V_BT_BLOCK (BLOCK_SIZE)
|
|
620
|
+
- RELAY_SWEEP_C2V_BT_WARPS (num_warps)
|
|
621
|
+
- RELAY_SWEEP_C2V_BT_STAGES (num_stages)
|
|
622
|
+
- RELAY_SWEEP_C2V_BTILE (BTILE)
|
|
623
|
+
Falls back to the shared BT defaults if unset.
|
|
624
|
+
"""
|
|
625
|
+
bs = _parse_list(
|
|
626
|
+
"RELAY_SWEEP_C2V_BT_BLOCK", _parse_list("RELAY_SWEEP_BT_BLOCK", [1, 2, 4, 8, 16, 32])
|
|
627
|
+
)
|
|
628
|
+
wp = _parse_list("RELAY_SWEEP_C2V_BT_WARPS", _parse_list("RELAY_SWEEP_BT_WARPS", [1, 2, 4]))
|
|
629
|
+
stg = _parse_list("RELAY_SWEEP_C2V_BT_STAGES", _parse_list("RELAY_SWEEP_BT_STAGES", [1, 2]))
|
|
630
|
+
bt = _parse_list("RELAY_SWEEP_C2V_BTILE", _parse_list("RELAY_SWEEP_BTILE", [16, 32, 64]))
|
|
631
|
+
space = {"BLOCK_SIZE": bs, "BTILE": bt, "num_warps": wp, "num_stages": stg}
|
|
632
|
+
return _make_configs(space)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
V2C_BTILE_BLOCK_SIZE = 1
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def build_v2c_btile_configs() -> list[triton.Config]:
|
|
639
|
+
"""Configs for V2C BTILE kernels (var-major, dimension ~V).
|
|
640
|
+
|
|
641
|
+
BLOCK_SIZE is always V2C_BTILE_BLOCK_SIZE and has no environment override.
|
|
642
|
+
|
|
643
|
+
Environment overrides (V2C-specific):
|
|
644
|
+
- RELAY_SWEEP_V2C_BT_WARPS (num_warps)
|
|
645
|
+
- RELAY_SWEEP_V2C_BT_STAGES (num_stages)
|
|
646
|
+
- RELAY_SWEEP_V2C_BTILE (BTILE)
|
|
647
|
+
Falls back to the shared BT defaults if unset.
|
|
648
|
+
"""
|
|
649
|
+
# BLOCK_SIZE stays 1: larger tiles change the summation order of a variable's
|
|
650
|
+
# incoming messages (with num_warps), so results would depend on the config.
|
|
651
|
+
bs = [V2C_BTILE_BLOCK_SIZE]
|
|
652
|
+
wp = _parse_list("RELAY_SWEEP_V2C_BT_WARPS", _parse_list("RELAY_SWEEP_BT_WARPS", [1, 2, 4]))
|
|
653
|
+
stg = _parse_list("RELAY_SWEEP_V2C_BT_STAGES", _parse_list("RELAY_SWEEP_BT_STAGES", [1, 2]))
|
|
654
|
+
bt = _parse_list("RELAY_SWEEP_V2C_BTILE", _parse_list("RELAY_SWEEP_BTILE", [16, 32, 64]))
|
|
655
|
+
space = {"BLOCK_SIZE": bs, "BTILE": bt, "num_warps": wp, "num_stages": stg}
|
|
656
|
+
return _make_configs(space)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def build_check_select_configs() -> list[triton.Config]:
|
|
660
|
+
"""Build configs for the check_and_select kernel.
|
|
661
|
+
|
|
662
|
+
Environment overrides:
|
|
663
|
+
- RELAY_SWEEP_CS_BLOCK_C: comma list for BLOCK_C (default: 64,128,256)
|
|
664
|
+
- RELAY_SWEEP_CS_BLOCK_V: comma list for BLOCK_V (default: 64,128,256)
|
|
665
|
+
- RELAY_SWEEP_CS_WARPS: comma list for num_warps (default: 1,2)
|
|
666
|
+
- RELAY_SWEEP_CS_STAGES: comma list for num_stages (default: 1,2)
|
|
667
|
+
"""
|
|
668
|
+
bc = _parse_list("RELAY_SWEEP_CS_BLOCK_C", [16, 32, 64])
|
|
669
|
+
bv = _parse_list("RELAY_SWEEP_CS_BLOCK_V", [16, 32, 64])
|
|
670
|
+
wp = _parse_list("RELAY_SWEEP_CS_WARPS", [1, 2, 4, 8])
|
|
671
|
+
st = _parse_list("RELAY_SWEEP_CS_STAGES", [1, 2, 3, 4])
|
|
672
|
+
space = {"BLOCK_C": bc, "BLOCK_V": bv, "num_warps": wp, "num_stages": st}
|
|
673
|
+
return _make_configs(space)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def build_btile_transpose_configs() -> list[triton.Config]:
|
|
677
|
+
btile = _parse_list("RELAY_SWEEP_TR_BTILE", [8, 16, 32, 64, 128, 256])
|
|
678
|
+
wp = _parse_list("RELAY_SWEEP_TR_WARPS", [1, 2, 4])
|
|
679
|
+
stg = _parse_list("RELAY_SWEEP_TR_STAGES", [1, 2])
|
|
680
|
+
space = {"BTILE": btile, "num_warps": wp, "num_stages": stg}
|
|
681
|
+
return _make_configs(space)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def build_parity_configs() -> list[triton.Config]:
|
|
685
|
+
bs = _parse_list("RELAY_SWEEP_PAR_BLOCK", [1, 2, 4, 16, 32, 64, 128, 256])
|
|
686
|
+
wp = _parse_list("RELAY_SWEEP_PAR_WARPS", [1, 2, 4])
|
|
687
|
+
stg = _parse_list("RELAY_SWEEP_PAR_STAGES", [1, 2])
|
|
688
|
+
space = {"BLOCK_SIZE": bs, "num_warps": wp, "num_stages": stg}
|
|
689
|
+
return _make_configs(space)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def build_permute_configs() -> list[triton.Config]:
|
|
693
|
+
"""Build autotune configurations for permute_rows_kernel.
|
|
694
|
+
|
|
695
|
+
The permute kernel copies edge rows in BTILE chunks, so we tune:
|
|
696
|
+
- BTILE: batch tile size for vectorized copying
|
|
697
|
+
- num_warps: number of warps per program
|
|
698
|
+
- num_stages: pipeline stages
|
|
699
|
+
"""
|
|
700
|
+
btile = _parse_list("RELAY_SWEEP_PERM_BTILE", [64, 128, 256, 512])
|
|
701
|
+
wp = _parse_list("RELAY_SWEEP_PERM_WARPS", [1, 2, 4, 8])
|
|
702
|
+
stg = _parse_list("RELAY_SWEEP_PERM_STAGES", [1, 2])
|
|
703
|
+
space = {"BTILE": btile, "num_warps": wp, "num_stages": stg}
|
|
704
|
+
return _make_configs(space)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def build_init_messages_configs() -> list[triton.Config]:
|
|
708
|
+
"""Build autotune configurations for init_messages_with_priors_EB_kernel.
|
|
709
|
+
|
|
710
|
+
The init kernel initializes messages (nu=zero, mu=priors) for all edges and batches.
|
|
711
|
+
This is a memory-bound operation, so we tune:
|
|
712
|
+
- BLOCK: block size for vectorized operations (affects memory coalescing)
|
|
713
|
+
- num_warps: number of warps per program (affects occupancy)
|
|
714
|
+
- num_stages: pipeline stages (affects memory latency hiding)
|
|
715
|
+
|
|
716
|
+
Environment overrides:
|
|
717
|
+
- RELAY_SWEEP_INIT_BLOCK: comma list for BLOCK (default: 256,512,1024,2048)
|
|
718
|
+
- RELAY_SWEEP_INIT_WARPS: comma list for num_warps (default: 1,2,4,8)
|
|
719
|
+
- RELAY_SWEEP_INIT_STAGES: comma list for num_stages (default: 1,2,3,4)
|
|
720
|
+
"""
|
|
721
|
+
bs = _parse_list("RELAY_SWEEP_INIT_BLOCK", [128, 256, 512, 1024, 2048])
|
|
722
|
+
wp = _parse_list("RELAY_SWEEP_INIT_WARPS", [2, 4, 8, 16])
|
|
723
|
+
stg = _parse_list("RELAY_SWEEP_INIT_STAGES", [1, 2, 3, 4])
|
|
724
|
+
space = {"BLOCK": bs, "num_warps": wp, "num_stages": stg}
|
|
725
|
+
return _make_configs(space)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def build_persistent_bp_configs() -> list[triton.Config]:
|
|
729
|
+
"""Build autotune configurations for persistent_bp_iterations_kernel.
|
|
730
|
+
|
|
731
|
+
The persistent kernel processes one batch lane at a time, running all iterations
|
|
732
|
+
inside a single kernel launch. It fuses C2V, V2C, parity, and selection operations.
|
|
733
|
+
|
|
734
|
+
We tune:
|
|
735
|
+
- BLOCK_SIZE: Vectorization block size for memory operations (affects coalescing)
|
|
736
|
+
- num_warps: Number of warps per program (affects occupancy and parallelism)
|
|
737
|
+
- num_stages: Pipeline stages (affects memory latency hiding)
|
|
738
|
+
|
|
739
|
+
Environment overrides:
|
|
740
|
+
- RELAY_SWEEP_PERSISTENT_BLOCK: comma list for BLOCK_SIZE (default: 16,32,64,128,256)
|
|
741
|
+
- RELAY_SWEEP_PERSISTENT_WARPS: comma list for num_warps (default: 1,2,4,8)
|
|
742
|
+
- RELAY_SWEEP_PERSISTENT_STAGES: comma list for num_stages (default: 1,2,3,4)
|
|
743
|
+
"""
|
|
744
|
+
bs = _parse_list("RELAY_SWEEP_PERSISTENT_BLOCK", [16, 32, 64, 128, 256])
|
|
745
|
+
wp = _parse_list("RELAY_SWEEP_PERSISTENT_WARPS", [1, 2, 4, 8])
|
|
746
|
+
stg = _parse_list("RELAY_SWEEP_PERSISTENT_STAGES", [1, 2, 3, 4])
|
|
747
|
+
space = {"BLOCK_SIZE": bs, "num_warps": wp, "num_stages": stg}
|
|
748
|
+
return _make_configs(space)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def build_c2v_grid_stride_configs() -> list[triton.Config]:
|
|
752
|
+
"""Build autotune configurations for c2v_min_sum_grid_stride_kernel.
|
|
753
|
+
|
|
754
|
+
This kernel only performs the C2V (check-to-variable) phase to ensure
|
|
755
|
+
memory consistency. V2C, parity, and selection phases run in separate kernel launches
|
|
756
|
+
after this kernel completes, providing implicit barriers (kernel launch completion)
|
|
757
|
+
between phases.
|
|
758
|
+
|
|
759
|
+
This kernel uses grid-stride parallelism (like CUDA) to split work across SMs efficiently.
|
|
760
|
+
|
|
761
|
+
We tune:
|
|
762
|
+
- BLOCK_SIZE: Vectorization block size for memory operations
|
|
763
|
+
- num_warps: Number of warps per program
|
|
764
|
+
- num_stages: Pipeline stages
|
|
765
|
+
- ROWS_PER_CHK: Number of checks processed per program (default: 1 for grid-stride)
|
|
766
|
+
- ROWS_PER_VAR: Number of vars processed per program (default: 1, unused in C2V-only)
|
|
767
|
+
|
|
768
|
+
Environment overrides (kept for backward compatibility):
|
|
769
|
+
- RELAY_SWEEP_ITERATION_FUSED_BLOCK: comma list for BLOCK_SIZE (default: 16,32,64,128)
|
|
770
|
+
- RELAY_SWEEP_ITERATION_FUSED_WARPS: comma list for num_warps (default: 1,2,4,8)
|
|
771
|
+
- RELAY_SWEEP_ITERATION_FUSED_STAGES: comma list for num_stages (default: 1,2,3)
|
|
772
|
+
- RELAY_SWEEP_ITERATION_FUSED_ROWS_CHK: comma list for ROWS_PER_CHK (default: 1,2,4,8)
|
|
773
|
+
- RELAY_SWEEP_ITERATION_FUSED_ROWS_VAR: comma list for ROWS_PER_VAR (default: 1,2,4,8)
|
|
774
|
+
|
|
775
|
+
Note: For small matrices, ROWS_PER_CHK=1 maximizes parallelism. For huge matrices,
|
|
776
|
+
ROWS_PER_CHK>1 can improve cache locality and reduce overhead. The autotuner will
|
|
777
|
+
select the optimal value based on actual performance.
|
|
778
|
+
"""
|
|
779
|
+
bs = _parse_list("RELAY_SWEEP_ITERATION_FUSED_BLOCK", [16, 32, 64, 128])
|
|
780
|
+
wp = _parse_list("RELAY_SWEEP_ITERATION_FUSED_WARPS", [1, 2, 4, 8])
|
|
781
|
+
stg = _parse_list("RELAY_SWEEP_ITERATION_FUSED_STAGES", [1, 2, 3])
|
|
782
|
+
# Make ROWS_PER_CHK and ROWS_PER_VAR tunable (like regular C2V/V2C kernels)
|
|
783
|
+
# Default includes 1 for grid-stride, but also allows 2,4,8 for better cache locality on huge matrices
|
|
784
|
+
r_chk = _parse_list("RELAY_SWEEP_ITERATION_FUSED_ROWS_CHK", [1, 2, 4, 8])
|
|
785
|
+
r_var = _parse_list("RELAY_SWEEP_ITERATION_FUSED_ROWS_VAR", [1, 2, 4, 8])
|
|
786
|
+
space = {
|
|
787
|
+
"BLOCK_SIZE": bs,
|
|
788
|
+
"ROWS_PER_CHK": r_chk,
|
|
789
|
+
"ROWS_PER_VAR": r_var,
|
|
790
|
+
"num_warps": wp,
|
|
791
|
+
"num_stages": stg,
|
|
792
|
+
}
|
|
793
|
+
return _make_configs(space)
|