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/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)