from __future__ import annotations import atexit import json import os from pathlib import Path from typing import Optional import torch try: import triton import triton.language as tl from triton.language.extra.cuda import libdevice as tl_libdevice _TRITON_AVAILABLE = True except Exception: # pragma: no cover triton = None # type: ignore tl = None # type: ignore tl_libdevice = None # type: ignore _TRITON_AVAILABLE = False _ENV_ENABLE = "WAN2GP_QUANTO_INT8_TRITON" _ENV_AUTOTUNE_ENABLE = "WAN2GP_QUANTO_INT8_AUTOTUNE" _ENV_AUTOTUNE_DEBUG = "WAN2GP_QUANTO_INT8_AUTOTUNE_DEBUG" _ENV_AUTOTUNE_MAX_M = "WAN2GP_QUANTO_INT8_AUTOTUNE_MAX_M" _ENV_AUTOTUNE_MAX_SHAPES = "WAN2GP_QUANTO_INT8_AUTOTUNE_MAX_SHAPES" _ENV_AUTOTUNE_WARMUP = "WAN2GP_QUANTO_INT8_AUTOTUNE_WARMUP" _ENV_AUTOTUNE_ITERS = "WAN2GP_QUANTO_INT8_AUTOTUNE_ITERS" _ENV_AUTOTUNE_MIN_SPEEDUP = "WAN2GP_QUANTO_INT8_AUTOTUNE_MIN_SPEEDUP" _ENV_AUTOTUNE_CACHE = "WAN2GP_QUANTO_INT8_AUTOTUNE_CACHE" _ENV_AUTOTUNE_VALIDATE = "WAN2GP_QUANTO_INT8_AUTOTUNE_VALIDATE" _ENV_AUTOTUNE_MAX_ABS_ERR = "WAN2GP_QUANTO_INT8_AUTOTUNE_MAX_ABS_ERR" _ENV_AUTOTUNE_MAX_REL_ERR = "WAN2GP_QUANTO_INT8_AUTOTUNE_MAX_REL_ERR" _ENV_AUTOTUNE_LOCK_FUSED_BLOCK_K = "WAN2GP_QUANTO_INT8_AUTOTUNE_LOCK_FUSED_BLOCK_K" _IS_AVAILABLE = None _CONFIG_LEN = 5 _AUTOTUNE_CACHE_VERSION = 2 _AUTOTUNE_CACHE_LOADED = False _AUTOTUNE_CACHE_DIRTY = False _AUTOTUNE_CONFIG_CACHE: dict[str, tuple[int, int, int, int, int]] = {} _AUTOTUNE_SESSION_CACHE: dict[tuple[int, str, str], tuple[int, int, int, int, int]] = {} _AUTOTUNE_SEEN_SLOTS: set[tuple[int, str, str]] = set() _AUTOTUNE_SLOTS_TUNED = 0 _AUTOTUNE_DEBUG_OVERRIDE: Optional[bool] = None # Tuned decode-time configs reused from nanovllm int8 kernels. _TRITON_SMALL_M_CONFIGS = { (2048, 4096): (2, 32, 256, 8, 5), (2048, 2048): (1, 32, 64, 2, 4), (2048, 12288): (8, 64, 256, 8, 4), (6144, 2048): (1, 32, 512, 4, 5), } _TRITON_TINY_M_SHAPE_CONFIGS = { (2, 3072, 3072): (2, 128, 64, 8, 4), (4, 3072, 3072): (4, 256, 64, 4, 4), (2, 3072, 1024): (2, 256, 128, 8, 4), (4, 3072, 1024): (2, 256, 128, 8, 4), (2, 3072, 1536): (2, 64, 64, 4, 4), (4, 3072, 1536): (2, 256, 128, 8, 4), (2, 3072, 8192): (2, 128, 64, 8, 4), (4, 3072, 8192): (2, 128, 64, 8, 4), (2, 8192, 3072): (4, 128, 64, 4, 4), (4, 8192, 3072): (2, 128, 128, 8, 4), } _TRITON_TINY_M_PAIR_CONFIGS = { (3072, 3072): (2, 128, 64, 8, 4), (3072, 1024): (2, 256, 128, 8, 4), (3072, 1536): (2, 64, 64, 4, 4), (3072, 8192): (2, 128, 64, 8, 4), (8192, 3072): (4, 128, 64, 4, 4), } _TRITON_SMALL_M_DEFAULT = (4, 256, 64, 4, 4) _TRITON_SMALL_M_K3072_DEFAULT = (2, 64, 64, 4, 4) _TRITON_MID_M_DEFAULT = (32, 128, 64, 8, 4) _TRITON_LARGE_M_DEFAULT = (64, 128, 64, 8, 4) _TRITON_LARGE_M_SHAPE_CONFIGS = { # Hot LTX2 distilled fused-int8 shapes profiled on RTX 50xx. (3840, 2048): (64, 256, 64, 8, 4), (3840, 15360): (64, 256, 64, 8, 4), (3840, 4096): (64, 256, 64, 8, 4), (4096, 3840): (64, 256, 64, 8, 4), (15360, 3840): (64, 256, 64, 8, 4), # Hot WAN2 I2V enhanced-lightning fused-int8 shapes (M ~= 512). (4096, 4096): (64, 256, 64, 8, 4), (4096, 10240): (64, 256, 64, 8, 4), (10240, 4096): (64, 256, 64, 8, 4), } _AUTOTUNE_SLOT_REPS = { "tiny_k3072_default": ((2, 3072, 2048), (4, 3072, 4096)), "tiny_default": ((2, 4096, 1536), (4, 4096, 1536)), "mid_default": ((32, 2048, 4096), (32, 3072, 3072), (32, 4096, 4096)), "large_n_ge_2048": ((512, 4096, 4096), (3840, 3840, 4096)), "large_default": ((128, 4096, 1024), (192, 3072, 1536)), } _RUNTIME_PROBE_MAX_N = 4096 def _env_flag(name: str, default: str = "1") -> bool: val = os.environ.get(name, default) return str(val).strip().lower() in ("1", "true", "yes", "on") def _parse_version(ver: str) -> tuple[int, int]: try: parts = ver.split(".") return int(parts[0]), int(parts[1]) except Exception: return (0, 0) def _is_stream_capturing() -> bool: if not torch.cuda.is_available(): return False try: return bool(torch.cuda.is_current_stream_capturing()) except Exception: return False def _env_int(name: str, default: int) -> int: try: return int(os.environ.get(name, str(default))) except Exception: return default def _env_float(name: str, default: float) -> float: try: return float(os.environ.get(name, str(default))) except Exception: return default def _autotune_debug(msg: str) -> None: if _AUTOTUNE_DEBUG_OVERRIDE is None: debug_on = _env_flag(_ENV_AUTOTUNE_DEBUG, "0") else: debug_on = bool(_AUTOTUNE_DEBUG_OVERRIDE) if debug_on: print(f"[WAN2GP][INT8][autotune] {msg}") def set_autotune_debug(enabled: Optional[bool] = None) -> None: global _AUTOTUNE_DEBUG_OVERRIDE _AUTOTUNE_DEBUG_OVERRIDE = None if enabled is None else bool(enabled) def _runtime_compatible() -> bool: if not (_TRITON_AVAILABLE and torch.cuda.is_available()): return False try: cc_major, _ = torch.cuda.get_device_capability() except Exception: return False # Triton int8 dot kernels require tensor-core generation GPUs. if cc_major < 8: return False # Keep SM120 safe on older Triton builds that abort at compile time. triton_ver = _parse_version(getattr(triton, "__version__", "0.0")) if cc_major >= 12 and triton_ver < (3, 6): return False return True def is_available() -> bool: global _IS_AVAILABLE if _IS_AVAILABLE is None: _IS_AVAILABLE = bool(_runtime_compatible() and _env_flag(_ENV_ENABLE, "1")) return _IS_AVAILABLE def _select_static_triton_int8_config(m: int, k: int, n: int) -> tuple[int, int, int, int, int]: if m <= 4: cfg = _TRITON_TINY_M_SHAPE_CONFIGS.get((m, k, n)) if cfg is not None: return cfg cfg = _TRITON_TINY_M_PAIR_CONFIGS.get((k, n)) if cfg is not None: return cfg cfg = _TRITON_SMALL_M_CONFIGS.get((k, n)) if cfg is not None: return cfg if k == 3072: return _TRITON_SMALL_M_K3072_DEFAULT return _TRITON_SMALL_M_DEFAULT if m < 64: return _TRITON_MID_M_DEFAULT if m >= 256: cfg = _TRITON_LARGE_M_SHAPE_CONFIGS.get((k, n)) if cfg is not None: return cfg if n >= 2048: return (64, 256, 64, 8, 4) return _TRITON_LARGE_M_DEFAULT def _dedup_shapes(shapes: tuple[tuple[int, int, int], ...]) -> tuple[tuple[int, int, int], ...]: out: list[tuple[int, int, int]] = [] seen: set[tuple[int, int, int]] = set() for shape in shapes: if not isinstance(shape, (list, tuple)) or len(shape) != 3: continue try: m, k, n = (int(shape[0]), int(shape[1]), int(shape[2])) except Exception: continue if m <= 0 or k <= 0 or n <= 0: continue key = (m, k, n) if key in seen: continue seen.add(key) out.append(key) return tuple(out) def _resolve_autotune_slot(m: int, k: int, n: int) -> tuple[str, tuple[tuple[int, int, int], ...]]: baseline = _select_static_triton_int8_config(m, k, n) if m <= 4: if (m, k, n) in _TRITON_TINY_M_SHAPE_CONFIGS: slot_id = f"tiny_shape|m={m}|k={k}|n={n}" reps = ((m, k, n),) elif (k, n) in _TRITON_TINY_M_PAIR_CONFIGS: slot_id = f"tiny_pair|k={k}|n={n}" reps = ((2, k, n), (4, k, n)) elif (k, n) in _TRITON_SMALL_M_CONFIGS: slot_id = f"tiny_small_pair|k={k}|n={n}" reps = ((2, k, n), (4, k, n)) elif k == 3072: slot_id = "tiny_k3072_default" reps = _AUTOTUNE_SLOT_REPS[slot_id] else: slot_id = "tiny_default" reps = _AUTOTUNE_SLOT_REPS[slot_id] elif m < 64: slot_id = "mid_default" reps = _AUTOTUNE_SLOT_REPS[slot_id] elif m >= 256 and (k, n) in _TRITON_LARGE_M_SHAPE_CONFIGS: slot_id = f"large_hot_pair|k={k}|n={n}" reps = ((512, k, n), (3840, k, n)) elif m >= 256 and n >= 2048: slot_id = "large_n_ge_2048" reps = _AUTOTUNE_SLOT_REPS[slot_id] else: slot_id = "large_default" reps = _AUTOTUNE_SLOT_REPS[slot_id] filtered = [shape for shape in _dedup_shapes(reps) if _select_static_triton_int8_config(shape[0], shape[1], shape[2]) == baseline] if len(filtered) == 0: filtered = [(m, k, n)] return slot_id, tuple(filtered) def _normalize_config(cfg) -> Optional[tuple[int, int, int, int, int]]: if not isinstance(cfg, (list, tuple)) or len(cfg) != _CONFIG_LEN: return None try: c0, c1, c2, c3, c4 = (int(v) for v in cfg) except Exception: return None if c0 <= 0 or c1 <= 0 or c2 <= 0 or c3 <= 0 or c4 <= 0: return None return (c0, c1, c2, c3, c4) def _autotune_cache_path() -> Path: default_path = str(Path.home() / ".triton" / "autotune" / "wan2gp_int8_autotune_cache.json") return Path(os.environ.get(_ENV_AUTOTUNE_CACHE, default_path)).expanduser() def _load_autotune_cache() -> None: global _AUTOTUNE_CACHE_LOADED, _AUTOTUNE_CONFIG_CACHE if _AUTOTUNE_CACHE_LOADED: return _AUTOTUNE_CACHE_LOADED = True cache_path = _autotune_cache_path() try: payload = json.loads(cache_path.read_text(encoding="utf-8")) except Exception: return entries = payload.get("entries", {}) if not isinstance(entries, dict): return parsed = {} for key, raw_cfg in entries.items(): if not isinstance(key, str): continue cfg = _normalize_config(raw_cfg) if cfg is not None: parsed[key] = cfg _AUTOTUNE_CONFIG_CACHE = parsed def _save_autotune_cache() -> None: global _AUTOTUNE_CACHE_DIRTY if not _AUTOTUNE_CACHE_DIRTY: return cache_path = _autotune_cache_path() try: cache_path.parent.mkdir(parents=True, exist_ok=True) tmp_path = Path(f"{cache_path}.tmp") payload = {"version": _AUTOTUNE_CACHE_VERSION, "entries": {key: list(cfg) for key, cfg in _AUTOTUNE_CONFIG_CACHE.items()}} tmp_path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8") tmp_path.replace(cache_path) _AUTOTUNE_CACHE_DIRTY = False except Exception as exc: _autotune_debug(f"cache write failed: {exc}") def _device_index(device: Optional[torch.device]) -> int: if device is not None and device.type == "cuda" and device.index is not None: return int(device.index) return int(torch.cuda.current_device()) def _device_fingerprint(device_index: int) -> str: props = torch.cuda.get_device_properties(device_index) triton_ver = getattr(triton, "__version__", "0.0") return ( f"{props.name}|cc={props.major}.{props.minor}|sm={props.multi_processor_count}|" f"torch={torch.__version__}|triton={triton_ver}|wan2gp_int8_cache_v={_AUTOTUNE_CACHE_VERSION}" ) def _autotune_slot_cache_key(device_index: int, kernel_kind: str, slot_id: str) -> str: return f"{_device_fingerprint(device_index)}|{kernel_kind}|slot={slot_id}" def _autotune_legacy_shape_cache_key(device_index: int, kernel_kind: str, m: int, k: int, n: int) -> str: return f"{_device_fingerprint(device_index)}|{kernel_kind}|{m}|{k}|{n}" def _get_cached_config(device_index: int, kernel_kind: str, slot_id: str, m: int, k: int, n: int) -> Optional[tuple[int, int, int, int, int]]: global _AUTOTUNE_CACHE_DIRTY _load_autotune_cache() slot_key = _autotune_slot_cache_key(device_index, kernel_kind, slot_id) cfg = _AUTOTUNE_CONFIG_CACHE.get(slot_key) if cfg is not None: return cfg legacy_key = _autotune_legacy_shape_cache_key(device_index, kernel_kind, m, k, n) legacy_cfg = _AUTOTUNE_CONFIG_CACHE.get(legacy_key) if legacy_cfg is None: return None _AUTOTUNE_CONFIG_CACHE[slot_key] = legacy_cfg _AUTOTUNE_CACHE_DIRTY = True return legacy_cfg def _set_cached_config(device_index: int, kernel_kind: str, slot_id: str, cfg: tuple[int, int, int, int, int]) -> None: global _AUTOTUNE_CACHE_DIRTY _load_autotune_cache() key = _autotune_slot_cache_key(device_index, kernel_kind, slot_id) if _AUTOTUNE_CONFIG_CACHE.get(key) == cfg: return _AUTOTUNE_CONFIG_CACHE[key] = cfg _AUTOTUNE_CACHE_DIRTY = True _save_autotune_cache() def _drop_cached_config(device_index: int, kernel_kind: str, slot_id: str, m: int, k: int, n: int) -> None: global _AUTOTUNE_CACHE_DIRTY _load_autotune_cache() keys = ( _autotune_slot_cache_key(device_index, kernel_kind, slot_id), _autotune_legacy_shape_cache_key(device_index, kernel_kind, m, k, n), ) removed = False for key in keys: if key in _AUTOTUNE_CONFIG_CACHE: del _AUTOTUNE_CONFIG_CACHE[key] removed = True if removed: _AUTOTUNE_CACHE_DIRTY = True def _config_compatible_with_baseline( kind: str, baseline: tuple[int, int, int, int, int], cfg: tuple[int, int, int, int, int], ) -> bool: if kind == "fused" and _env_flag(_ENV_AUTOTUNE_LOCK_FUSED_BLOCK_K, "1"): # Fused blockscale kernel computes row scales per K-chunk; changing block_k changes numerics. return int(cfg[2]) == int(baseline[2]) return True def _runtime_probe_shape( kind: str, m: int, k: int, n: int, baseline: tuple[int, int, int, int, int], ) -> tuple[int, int, int, tuple[int, int, int, int, int]]: if n <= _RUNTIME_PROBE_MAX_N: return m, k, n, baseline probe_candidates = [] for probe_n in (_RUNTIME_PROBE_MAX_N, 3072, 2048, 1536, 1024, 768, 512, 256, 128, 64): if probe_n >= n: continue if probe_n % 8 != 0: continue probe_candidates.append(probe_n) for probe_n in probe_candidates: probe_baseline = _select_static_triton_int8_config(m, k, probe_n) if not _config_compatible_with_baseline(kind, baseline, probe_baseline): continue if not _config_compatible_with_baseline(kind, probe_baseline, baseline): continue return m, k, probe_n, probe_baseline return m, k, n, baseline def _candidate_configs( baseline: tuple[int, int, int, int, int], m: int, k: int, n: int, *, kind: str, ) -> list[tuple[int, int, int, int, int]]: out = [baseline] if m <= 4: out.extend( [ (1, 64, 64, 2, 4), (1, 128, 64, 4, 4), (2, 64, 64, 4, 4), (2, 128, 64, 4, 4), (2, 128, 128, 8, 4), (2, 256, 64, 8, 4), (4, 128, 64, 4, 4), (4, 256, 64, 4, 4), (8, 128, 64, 4, 4), ] ) shape_cfg = _TRITON_TINY_M_SHAPE_CONFIGS.get((m, k, n)) if shape_cfg is not None: out.append(shape_cfg) pair_cfg = _TRITON_TINY_M_PAIR_CONFIGS.get((k, n)) if pair_cfg is not None: out.append(pair_cfg) elif m <= 16: out.extend([(8, 128, 64, 4, 4), (8, 256, 64, 8, 4), (16, 128, 64, 8, 4), (16, 256, 64, 8, 4), (32, 128, 64, 8, 4)]) dedup: list[tuple[int, int, int, int, int]] = [] seen = set() for cfg in out: norm = _normalize_config(cfg) if norm is None or norm in seen: continue if not _config_compatible_with_baseline(kind, baseline, norm): continue seen.add(norm) dedup.append(norm) return dedup def _looks_like_unsupported_dot_tile(cfg: tuple[int, int, int, int, int]) -> bool: block_m, block_n, block_k, _, _ = cfg # Triton int8 dot kernels can reject tiny tiles on some runtimes (e.g. decode-time M<=4). return block_m < 16 or block_n < 16 or block_k < 32 def _compile_recovery_candidates( kind: str, baseline: tuple[int, int, int, int, int], preferred: tuple[int, int, int, int, int], m: int, k: int, n: int, ) -> list[tuple[int, int, int, int, int]]: block_k = max(32, int(baseline[2])) if block_k % 32 != 0: block_k = ((block_k + 31) // 32) * 32 conservative_large_tiles = [ (16, 32, block_k, 4, 4), (16, 64, block_k, 4, 4), (16, 128, block_k, 4, 4), (32, 32, block_k, 4, 4), (32, 64, block_k, 8, 4), (32, 128, block_k, 8, 4), (64, 64, block_k, 8, 4), (64, 128, block_k, 8, 4), ] raw = [preferred] raw.extend(_candidate_configs(baseline, m, k, n, kind=kind)) raw.extend(conservative_large_tiles) dedup: list[tuple[int, int, int, int, int]] = [] seen = set() for cfg in raw: norm = _normalize_config(cfg) if norm is None or norm in seen: continue if not _config_compatible_with_baseline(kind, baseline, norm): continue seen.add(norm) dedup.append(norm) if baseline not in dedup: dedup.append(baseline) if len(dedup) <= 1: return dedup head = dedup[0] tail = dedup[1:] non_tiny = [cfg for cfg in tail if not _looks_like_unsupported_dot_tile(cfg)] tiny = [cfg for cfg in tail if _looks_like_unsupported_dot_tile(cfg)] return [head, *non_tiny, *tiny] def _launch_candidate(kind: str, cfg: tuple[int, int, int, int, int], tensors: tuple[torch.Tensor, ...], m: int, n: int, k: int) -> None: block_m, block_n, block_k, num_warps, num_stages = cfg grid = (triton.cdiv(m, block_m), triton.cdiv(n, block_n)) if kind == "fused": x_mm_c, qweight_c, b_scale_c, out = tensors _fused_dynamic_int8_blockscale_gemm_kernel[grid]( x_mm_c, qweight_c, b_scale_c, out, m, n, k, x_mm_c.stride(0), x_mm_c.stride(1), qweight_c.stride(0), qweight_c.stride(1), out.stride(0), out.stride(1), block_m=block_m, block_n=block_n, block_k=block_k, num_warps=num_warps, num_stages=num_stages, ) return a_int8_c, b_int8_c, a_scale_c, b_scale_c, out = tensors _scaled_int8_gemm_kernel[grid]( a_int8_c, b_int8_c, a_scale_c, b_scale_c, out, m, n, k, a_int8_c.stride(0), a_int8_c.stride(1), b_int8_c.stride(0), b_int8_c.stride(1), out.stride(0), out.stride(1), block_m=block_m, block_n=block_n, block_k=block_k, num_warps=num_warps, num_stages=num_stages, ) def _create_bench_tensors(kind: str, device: torch.device, m: int, k: int, n: int) -> tuple[torch.Tensor, ...]: if kind == "fused": x_mm_c = torch.randn((m, k), device=device, dtype=torch.bfloat16) qweight_c = torch.randint(-128, 128, (n, k), device=device, dtype=torch.int8) b_scale_c = torch.rand((n,), device=device, dtype=torch.float32).add_(1e-4) out = torch.empty((m, n), device=device, dtype=torch.bfloat16) return (x_mm_c, qweight_c, b_scale_c, out) a_int8_c = torch.randint(-128, 128, (m, k), device=device, dtype=torch.int8) b_int8_c = torch.randint(-128, 128, (n, k), device=device, dtype=torch.int8) a_scale_c = torch.rand((m,), device=device, dtype=torch.float32).add_(1e-4) b_scale_c = torch.rand((n,), device=device, dtype=torch.float32).add_(1e-4) out = torch.empty((m, n), device=device, dtype=torch.bfloat16) return (a_int8_c, b_int8_c, a_scale_c, b_scale_c, out) def _run_candidate_once_with_error( kind: str, cfg: tuple[int, int, int, int, int], tensors: tuple[torch.Tensor, ...], m: int, k: int, n: int, ) -> tuple[Optional[torch.Tensor], Optional[Exception]]: try: if kind == "fused": x_mm_c, qweight_c, b_scale_c, _ = tensors out = torch.empty((m, n), device=x_mm_c.device, dtype=torch.bfloat16) _launch_candidate(kind, cfg, (x_mm_c, qweight_c, b_scale_c, out), m, n, k) torch.cuda.synchronize(x_mm_c.device) return out, None a_int8_c, b_int8_c, a_scale_c, b_scale_c, _ = tensors out = torch.empty((m, n), device=a_int8_c.device, dtype=torch.bfloat16) _launch_candidate(kind, cfg, (a_int8_c, b_int8_c, a_scale_c, b_scale_c, out), m, n, k) torch.cuda.synchronize(a_int8_c.device) return out, None except Exception as exc: _autotune_debug(f"single-run failed for {kind} shape=({m},{k},{n}) cfg={cfg}: {exc}") return None, exc def _run_candidate_once(kind: str, cfg: tuple[int, int, int, int, int], tensors: tuple[torch.Tensor, ...], m: int, k: int, n: int) -> Optional[torch.Tensor]: out, _ = _run_candidate_once_with_error(kind, cfg, tensors, m, k, n) return out def _ensure_compile_compatible_config( kind: str, device_index: int, slot_id: str, preferred: tuple[int, int, int, int, int], baseline: tuple[int, int, int, int, int], m: int, k: int, n: int, rep_shapes: tuple[tuple[int, int, int], ...], ) -> tuple[tuple[int, int, int, int, int], Optional[Exception]]: device = torch.device("cuda", device_index) _ = rep_shapes probe_m, probe_k, probe_n, _ = _runtime_probe_shape(kind, m, k, n, baseline) # Probe a reduced runtime-compatible shape when possible so compile validation does # not allocate fake giant heads (for example lm_head with 248k vocab rows). probe_shapes = ((probe_m, probe_k, probe_n),) tensors_by_shape = {shape: _create_bench_tensors(kind, device, *shape) for shape in probe_shapes} candidates = _compile_recovery_candidates(kind, baseline, preferred, m, k, n) last_error: Optional[Exception] = None for cfg in candidates: all_ok = True for probe_m, probe_k, probe_n in probe_shapes: _, probe_err = _run_candidate_once_with_error( kind, cfg, tensors_by_shape[(probe_m, probe_k, probe_n)], probe_m, probe_k, probe_n, ) if probe_err is not None: all_ok = False last_error = probe_err break if all_ok: if cfg != preferred: _autotune_debug( f"compile recovery picked {cfg} for {kind} slot={slot_id} shape=({m},{k},{n}) " f"instead of {preferred}" ) return cfg, None if last_error is not None: _autotune_debug( f"compile recovery failed for {kind} slot={slot_id} shape=({m},{k},{n}); " f"keeping {preferred}. last_error={last_error}" ) return preferred, last_error def _candidate_matches_baseline( baseline_out: torch.Tensor, candidate_out: torch.Tensor, *, max_abs_limit: float, rel_limit: float, ) -> tuple[bool, float, float]: if not torch.isfinite(candidate_out).all().item(): return False, float("inf"), float("inf") base_f = baseline_out.float() cand_f = candidate_out.float() diff = (base_f - cand_f).abs() max_abs = float(diff.max().item()) denom = base_f.abs().mean().clamp_min(1e-6) rel = float((diff.mean() / denom).item()) return (max_abs <= max_abs_limit and rel <= rel_limit), max_abs, rel def _validate_config( kind: str, device: torch.device, m: int, k: int, n: int, baseline: tuple[int, int, int, int, int], cfg: tuple[int, int, int, int, int], ) -> bool: if not _config_compatible_with_baseline(kind, baseline, cfg): return False if cfg == baseline: return True if not _env_flag(_ENV_AUTOTUNE_VALIDATE, "1"): return True max_abs_limit = max(0.0, _env_float(_ENV_AUTOTUNE_MAX_ABS_ERR, 0.25)) rel_limit = max(0.0, _env_float(_ENV_AUTOTUNE_MAX_REL_ERR, 0.001)) tensors = _create_bench_tensors(kind, device, m, k, n) baseline_out = _run_candidate_once(kind, baseline, tensors, m, k, n) candidate_out = _run_candidate_once(kind, cfg, tensors, m, k, n) if baseline_out is None or candidate_out is None: return False ok, max_abs, rel = _candidate_matches_baseline( baseline_out, candidate_out, max_abs_limit=max_abs_limit, rel_limit=rel_limit, ) if not ok: _autotune_debug( f"rejecting config {cfg} for {kind} shape=({m},{k},{n}) " f"vs baseline {baseline}: max_abs={max_abs:.6f}, rel={rel:.6f}" ) return ok def _benchmark_config_ms(kind: str, cfg: tuple[int, int, int, int, int], tensors: tuple[torch.Tensor, ...], device: torch.device, m: int, k: int, n: int) -> Optional[float]: warmup = max(1, _env_int(_ENV_AUTOTUNE_WARMUP, 2)) iters = max(1, _env_int(_ENV_AUTOTUNE_ITERS, 5)) try: for _ in range(warmup): _launch_candidate(kind, cfg, tensors, m, n, k) torch.cuda.synchronize(device) start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() for _ in range(iters): _launch_candidate(kind, cfg, tensors, m, n, k) end.record() end.synchronize() return float(start.elapsed_time(end)) / float(iters) except Exception as exc: _autotune_debug(f"benchmark failed for {kind} shape=({m},{k},{n}) cfg={cfg}: {exc}") return None def _can_tune_slot(slot_key: tuple[int, str, str]) -> bool: global _AUTOTUNE_SLOTS_TUNED if slot_key in _AUTOTUNE_SEEN_SLOTS: return True max_shapes = max(0, _env_int(_ENV_AUTOTUNE_MAX_SHAPES, 32)) if _AUTOTUNE_SLOTS_TUNED >= max_shapes: return False _AUTOTUNE_SEEN_SLOTS.add(slot_key) _AUTOTUNE_SLOTS_TUNED += 1 return True def _benchmark_slot_config_ms( kind: str, cfg: tuple[int, int, int, int, int], device: torch.device, rep_shapes: tuple[tuple[int, int, int], ...], ) -> Optional[float]: total = 0.0 count = 0 for rep_m, rep_k, rep_n in rep_shapes: rep_baseline = _select_static_triton_int8_config(rep_m, rep_k, rep_n) if not _validate_config(kind, device, rep_m, rep_k, rep_n, rep_baseline, cfg): return None tensors = _create_bench_tensors(kind, device, rep_m, rep_k, rep_n) ms = _benchmark_config_ms(kind, cfg, tensors, device, rep_m, rep_k, rep_n) if ms is None: return None total += ms count += 1 if count == 0: return None return total / float(count) def _autotune_config( kind: str, device_index: int, m: int, k: int, n: int, baseline: tuple[int, int, int, int, int], slot_id: str, rep_shapes: tuple[tuple[int, int, int], ...], ) -> tuple[int, int, int, int, int]: device = torch.device("cuda", device_index) cached = _get_cached_config(device_index, kind, slot_id, m, k, n) if cached is not None: probe_m, probe_k, probe_n, probe_baseline = _runtime_probe_shape(kind, m, k, n, baseline) if _validate_config(kind, device, probe_m, probe_k, probe_n, probe_baseline, cached): return cached _drop_cached_config(device_index, kind, slot_id, m, k, n) slot_key = (device_index, kind, slot_id) if not _can_tune_slot(slot_key): _autotune_debug(f"slot budget reached; keeping baseline for {kind} slot={slot_id} shape=({m},{k},{n})") return baseline rep_m, rep_k, rep_n = rep_shapes[0] rep_baseline = _select_static_triton_int8_config(rep_m, rep_k, rep_n) candidate_seed = rep_baseline if _config_compatible_with_baseline(kind, baseline, rep_baseline) else baseline candidates = _candidate_configs(candidate_seed, rep_m, rep_k, rep_n, kind=kind) if baseline not in candidates: candidates = [baseline, *candidates] results: dict[tuple[int, int, int, int, int], float] = {} for cfg in candidates: ms = _benchmark_slot_config_ms(kind, cfg, device, rep_shapes) if ms is not None: results[cfg] = ms baseline_ms = results.get(baseline) if baseline_ms is None: if len(results) > 0: recovered_cfg, recovered_ms = min(results.items(), key=lambda item: item[1]) _set_cached_config(device_index, kind, slot_id, recovered_cfg) _autotune_debug( f"baseline config failed for {kind} slot={slot_id} shape=({m},{k},{n}); " f"using first compilable cfg={recovered_cfg} (ms={recovered_ms:.4f})" ) return recovered_cfg _set_cached_config(device_index, kind, slot_id, baseline) _autotune_debug( f"no compilable configs found during autotune for {kind} slot={slot_id} shape=({m},{k},{n}); " f"keeping baseline {baseline}" ) return baseline best_cfg, best_ms = min(results.items(), key=lambda item: item[1]) min_speedup = max(1.0, _env_float(_ENV_AUTOTUNE_MIN_SPEEDUP, 1.02)) use_best = best_cfg != baseline and best_ms > 0.0 and (baseline_ms / best_ms) >= min_speedup picked = best_cfg if use_best else baseline probe_m, probe_k, probe_n, probe_baseline = _runtime_probe_shape(kind, m, k, n, baseline) if not _validate_config(kind, device, probe_m, probe_k, probe_n, probe_baseline, picked): picked = baseline _set_cached_config(device_index, kind, slot_id, picked) if use_best: _autotune_debug( f"picked {picked} over baseline {baseline} for {kind} slot={slot_id} shape=({m},{k},{n}), " f"baseline_ms={baseline_ms:.4f}, tuned_ms={best_ms:.4f}, speedup={baseline_ms / best_ms:.3f}x" ) else: _autotune_debug( f"kept baseline {baseline} for {kind} slot={slot_id} shape=({m},{k},{n}), " f"baseline_ms={baseline_ms:.4f}, best_cfg={best_cfg}, best_ms={best_ms:.4f}" ) return picked def _select_triton_int8_config( m: int, k: int, n: int, *, device: Optional[torch.device] = None, kernel_kind: str = "fused", ) -> tuple[int, int, int, int, int]: baseline = _select_static_triton_int8_config(m, k, n) if not is_available() or not torch.cuda.is_available(): return baseline try: device_index = _device_index(device) except Exception: return baseline slot_id, rep_shapes = _resolve_autotune_slot(m, k, n) session_key = (device_index, kernel_kind, slot_id) cached = _AUTOTUNE_SESSION_CACHE.get(session_key) if cached is not None: if _config_compatible_with_baseline(kernel_kind, baseline, cached): return cached _AUTOTUNE_SESSION_CACHE.pop(session_key, None) cached_cfg = _get_cached_config(device_index, kernel_kind, slot_id, m, k, n) if cached_cfg is not None and _config_compatible_with_baseline(kernel_kind, baseline, cached_cfg): if not _is_stream_capturing(): compile_safe, _ = _ensure_compile_compatible_config( kernel_kind, device_index, slot_id, cached_cfg, baseline, m, k, n, rep_shapes, ) if compile_safe != cached_cfg: _set_cached_config(device_index, kernel_kind, slot_id, compile_safe) _AUTOTUNE_SESSION_CACHE[session_key] = compile_safe return compile_safe _AUTOTUNE_SESSION_CACHE[session_key] = cached_cfg return cached_cfg if _is_stream_capturing(): # During graph capture we must avoid autotune/probing allocations. Do not populate # session cache with the baseline fallback, so a later non-capture call can autotune. return baseline autotune_enabled = _env_flag(_ENV_AUTOTUNE_ENABLE, "1") max_m = _env_int(_ENV_AUTOTUNE_MAX_M, -1) if autotune_enabled and not (max_m >= 0 and m > max_m): preferred = _autotune_config(kernel_kind, device_index, m, k, n, baseline, slot_id, rep_shapes) else: preferred = baseline compile_safe, compile_err = _ensure_compile_compatible_config( kernel_kind, device_index, slot_id, preferred, baseline, m, k, n, rep_shapes, ) picked = compile_safe if compile_safe != preferred: _set_cached_config(device_index, kernel_kind, slot_id, compile_safe) elif compile_err is not None: _autotune_debug( f"compile probe could not find an alternative for {kernel_kind} slot={slot_id} " f"shape=({m},{k},{n}); will keep {preferred}" ) _AUTOTUNE_SESSION_CACHE[session_key] = picked return picked atexit.register(_save_autotune_cache) if _TRITON_AVAILABLE: @triton.jit def _fused_dynamic_int8_gemm_kernel( a_ptr, b_ptr, s_ptr, c_ptr, m, n, k, stride_am, stride_ak, stride_bn, stride_bk, stride_cm, stride_cn, block_m: tl.constexpr, block_n: tl.constexpr, block_k: tl.constexpr, ): pid_m = tl.program_id(0) pid_n = tl.program_id(1) offs_m = pid_m * block_m + tl.arange(0, block_m) offs_n = pid_n * block_n + tl.arange(0, block_n) offs_k = tl.arange(0, block_k) # Pass 1: rowwise absmax for dynamic symmetric int8 activation quantization. row_amax = tl.zeros((block_m,), dtype=tl.float32) for k0 in range(0, k, block_k): kk = k0 + offs_k a = tl.load( a_ptr + offs_m[:, None] * stride_am + kk[None, :] * stride_ak, mask=(offs_m[:, None] < m) & (kk[None, :] < k), other=0, ).to(tl.float32) row_amax = tl.maximum(row_amax, tl.max(tl.abs(a), axis=1)) row_scale = row_amax / 127.0 row_scale = tl.where(row_scale > 0.0, row_scale, 1.0) row_inv_scale = 1.0 / row_scale # Pass 2: quantize activations on the fly + int8 dot. acc = tl.zeros((block_m, block_n), dtype=tl.int32) for k0 in range(0, k, block_k): kk = k0 + offs_k a = tl.load( a_ptr + offs_m[:, None] * stride_am + kk[None, :] * stride_ak, mask=(offs_m[:, None] < m) & (kk[None, :] < k), other=0, ).to(tl.float32) a = a * row_inv_scale[:, None] # Match torch.round behavior (ties-to-even) used by quanto::quantize_symmetric. a = tl_libdevice.rint(a) a = tl.maximum(tl.minimum(a, 127.0), -128.0).to(tl.int8) # Weight is [N, K]; load as [K, N] tile for dot. b = tl.load( b_ptr + offs_n[None, :] * stride_bn + kk[:, None] * stride_bk, mask=(offs_n[None, :] < n) & (kk[:, None] < k), other=0, ).to(tl.int8) acc += tl.dot(a, b) scales = tl.load(s_ptr + offs_n, mask=offs_n < n, other=0).to(tl.float32) out = acc.to(tl.float32) * row_scale[:, None] * scales[None, :] tl.store( c_ptr + offs_m[:, None] * stride_cm + offs_n[None, :] * stride_cn, out, mask=(offs_m[:, None] < m) & (offs_n[None, :] < n), ) @triton.jit def _fused_dynamic_int8_blockscale_gemm_kernel( a_ptr, b_ptr, s_ptr, c_ptr, m, n, k, stride_am, stride_ak, stride_bn, stride_bk, stride_cm, stride_cn, block_m: tl.constexpr, block_n: tl.constexpr, block_k: tl.constexpr, ): pid_m = tl.program_id(0) pid_n = tl.program_id(1) offs_m = pid_m * block_m + tl.arange(0, block_m) offs_n = pid_n * block_n + tl.arange(0, block_n) offs_k = tl.arange(0, block_k) acc = tl.zeros((block_m, block_n), dtype=tl.float32) for k0 in range(0, k, block_k): kk = k0 + offs_k a = tl.load( a_ptr + offs_m[:, None] * stride_am + kk[None, :] * stride_ak, mask=(offs_m[:, None] < m) & (kk[None, :] < k), other=0, ).to(tl.float32) row_amax = tl.max(tl.abs(a), axis=1) row_scale = row_amax / 127.0 row_scale = tl.where(row_scale > 0.0, row_scale, 1.0) a = a / row_scale[:, None] a = tl_libdevice.rint(a) a = tl.maximum(tl.minimum(a, 127.0), -128.0).to(tl.int8) b = tl.load( b_ptr + offs_n[None, :] * stride_bn + kk[:, None] * stride_bk, mask=(offs_n[None, :] < n) & (kk[:, None] < k), other=0, ).to(tl.int8) dot_i32 = tl.dot(a, b) acc += dot_i32.to(tl.float32) * row_scale[:, None] scales = tl.load(s_ptr + offs_n, mask=offs_n < n, other=0).to(tl.float32) out = acc * scales[None, :] tl.store( c_ptr + offs_m[:, None] * stride_cm + offs_n[None, :] * stride_cn, out, mask=(offs_m[:, None] < m) & (offs_n[None, :] < n), ) @triton.jit def _scaled_int8_gemm_kernel( a_ptr, b_ptr, a_scales_ptr, b_scales_ptr, c_ptr, m, n, k, stride_am, stride_ak, stride_bn, stride_bk, stride_cm, stride_cn, block_m: tl.constexpr, block_n: tl.constexpr, block_k: tl.constexpr, ): pid_m = tl.program_id(0) pid_n = tl.program_id(1) offs_m = pid_m * block_m + tl.arange(0, block_m) offs_n = pid_n * block_n + tl.arange(0, block_n) offs_k = tl.arange(0, block_k) acc = tl.zeros((block_m, block_n), dtype=tl.int32) for k0 in range(0, k, block_k): kk = k0 + offs_k a = tl.load( a_ptr + offs_m[:, None] * stride_am + kk[None, :] * stride_ak, mask=(offs_m[:, None] < m) & (kk[None, :] < k), other=0, ).to(tl.int8) # Weight is [N, K]; load as [K, N] tile for dot. b = tl.load( b_ptr + offs_n[None, :] * stride_bn + kk[:, None] * stride_bk, mask=(offs_n[None, :] < n) & (kk[:, None] < k), other=0, ).to(tl.int8) acc += tl.dot(a, b) a_scales = tl.load(a_scales_ptr + offs_m, mask=offs_m < m, other=1).to(tl.float32) b_scales = tl.load(b_scales_ptr + offs_n, mask=offs_n < n, other=1).to(tl.float32) out = acc.to(tl.float32) * a_scales[:, None] * b_scales[None, :] tl.store( c_ptr + offs_m[:, None] * stride_cm + offs_n[None, :] * stride_cn, out, mask=(offs_m[:, None] < m) & (offs_n[None, :] < n), ) def _flatten_scale(scale: torch.Tensor) -> torch.Tensor: if scale.ndim == 2 and scale.shape[1] == 1: return scale.view(-1) if scale.ndim == 1: return scale return scale.reshape(-1) def _expand_or_validate_scale(scale: torch.Tensor, expected: int) -> torch.Tensor: scale = _flatten_scale(scale) if scale.numel() == 1: return scale.reshape(1).expand(expected) if scale.numel() != expected: raise RuntimeError(f"Scale length mismatch: expected {expected}, got {scale.numel()}") return scale def _fused_quant_scaled_mm_common( x2d: torch.Tensor, qweight: torch.Tensor, b_scale: torch.Tensor, *, k: int, n: int, stride_bn: int, stride_bk: int, out_dtype: torch.dtype, ) -> torch.Tensor: m = x2d.shape[0] out = torch.empty((m, n), device=x2d.device, dtype=out_dtype) x_mm_c = x2d if x2d.is_contiguous() else x2d.contiguous() b_scale_c = b_scale if b_scale.is_contiguous() else b_scale.contiguous() block_m, block_n, block_k, num_warps, num_stages = _select_triton_int8_config(m, k, n, device=x2d.device, kernel_kind="fused") grid = (triton.cdiv(m, block_m), triton.cdiv(n, block_n)) _fused_dynamic_int8_blockscale_gemm_kernel[grid]( x_mm_c, qweight, b_scale_c, out, m, n, k, x_mm_c.stride(0), x_mm_c.stride(1), stride_bn, stride_bk, out.stride(0), out.stride(1), block_m=block_m, block_n=block_n, block_k=block_k, num_warps=num_warps, num_stages=num_stages, ) return out def fused_quant_scaled_mm( x2d: torch.Tensor, qweight: torch.Tensor, qweight_scale: torch.Tensor, out_dtype: Optional[torch.dtype] = None, ) -> torch.Tensor: if not is_available(): raise RuntimeError("Triton backend not available") if x2d.ndim != 2: raise RuntimeError("x2d must be 2D") if qweight.ndim != 2: raise RuntimeError("qweight must be 2D [N, K]") if x2d.dtype not in (torch.bfloat16, torch.float16, torch.float32): raise RuntimeError("x2d must be bf16/fp16/fp32") if qweight.dtype != torch.int8: raise RuntimeError("qweight must be int8") if not x2d.is_cuda or not qweight.is_cuda: raise RuntimeError("fused_quant_scaled_mm requires CUDA tensors") m, k = x2d.shape n, k2 = qweight.shape if k != k2: raise RuntimeError(f"Triton int8 GEMM shape mismatch: x={x2d.shape}, w={qweight.shape}") b_scale = _expand_or_validate_scale(qweight_scale, n) if b_scale.device != x2d.device or b_scale.dtype != torch.float32: b_scale = b_scale.to(device=x2d.device, dtype=torch.float32) elif not b_scale.is_contiguous(): b_scale = b_scale.contiguous() if x2d.dtype not in (torch.bfloat16, torch.float16, torch.float32): raise RuntimeError(f"Unsupported activation dtype for fused path: {x2d.dtype}") out_dtype = out_dtype or x2d.dtype qweight_c = qweight if qweight.is_contiguous() else qweight.contiguous() return _fused_quant_scaled_mm_common( x2d, qweight_c, b_scale, k=k, n=n, stride_bn=qweight_c.stride(0), stride_bk=qweight_c.stride(1), out_dtype=out_dtype, ) def fused_quant_scaled_mm_transposed( x2d: torch.Tensor, qweight_t: torch.Tensor, qweight_scale: torch.Tensor, out_dtype: Optional[torch.dtype] = None, ) -> torch.Tensor: if not is_available(): raise RuntimeError("Triton backend not available") if x2d.ndim != 2: raise RuntimeError("x2d must be 2D") if qweight_t.ndim != 2: raise RuntimeError("qweight_t must be 2D [K, N]") if x2d.dtype not in (torch.bfloat16, torch.float16, torch.float32): raise RuntimeError("x2d must be bf16/fp16/fp32") if qweight_t.dtype != torch.int8: raise RuntimeError("qweight_t must be int8") if not x2d.is_cuda or not qweight_t.is_cuda: raise RuntimeError("fused_quant_scaled_mm_transposed requires CUDA tensors") m, k = x2d.shape k2, n = qweight_t.shape if k != k2: raise RuntimeError(f"Triton int8 GEMM shape mismatch: x={x2d.shape}, w_t={qweight_t.shape}") b_scale = _expand_or_validate_scale(qweight_scale, n) if b_scale.device != x2d.device or b_scale.dtype != torch.float32: b_scale = b_scale.to(device=x2d.device, dtype=torch.float32) elif not b_scale.is_contiguous(): b_scale = b_scale.contiguous() if x2d.dtype not in (torch.bfloat16, torch.float16, torch.float32): raise RuntimeError(f"Unsupported activation dtype for fused path: {x2d.dtype}") out_dtype = out_dtype or x2d.dtype qweight_t_c = qweight_t if qweight_t.is_contiguous() else qweight_t.contiguous() return _fused_quant_scaled_mm_common( x2d, qweight_t_c, b_scale, k=k, n=n, stride_bn=qweight_t_c.stride(1), stride_bk=qweight_t_c.stride(0), out_dtype=out_dtype, ) def scaled_int8_mm( a_int8: torch.Tensor, b_int8: torch.Tensor, a_scale: torch.Tensor, b_scale: torch.Tensor, out_dtype: Optional[torch.dtype] = None, ) -> torch.Tensor: if not is_available(): raise RuntimeError("Triton backend not available") if a_int8.ndim != 2: raise RuntimeError("a_int8 must be 2D") if b_int8.ndim != 2: raise RuntimeError("b_int8 must be 2D [N, K]") if a_int8.dtype != torch.int8 or b_int8.dtype != torch.int8: raise RuntimeError("scaled_int8_mm requires int8 activations and int8 weights") if not a_int8.is_cuda or not b_int8.is_cuda: raise RuntimeError("scaled_int8_mm requires CUDA tensors") m, k = a_int8.shape n, k2 = b_int8.shape if k != k2: raise RuntimeError(f"Triton int8 GEMM shape mismatch: a={a_int8.shape}, w={b_int8.shape}") a_scale = _expand_or_validate_scale(a_scale, m) b_scale = _expand_or_validate_scale(b_scale, n) if a_scale.device != a_int8.device or a_scale.dtype != torch.float32: a_scale = a_scale.to(device=a_int8.device, dtype=torch.float32) elif not a_scale.is_contiguous(): a_scale = a_scale.contiguous() if b_scale.device != a_int8.device or b_scale.dtype != torch.float32: b_scale = b_scale.to(device=a_int8.device, dtype=torch.float32) elif not b_scale.is_contiguous(): b_scale = b_scale.contiguous() out_dtype = out_dtype or torch.bfloat16 out = torch.empty((m, n), device=a_int8.device, dtype=out_dtype) a_int8_c = a_int8 if a_int8.is_contiguous() else a_int8.contiguous() b_int8_c = b_int8 if b_int8.is_contiguous() else b_int8.contiguous() a_scale_c = a_scale if a_scale.is_contiguous() else a_scale.contiguous() b_scale_c = b_scale if b_scale.is_contiguous() else b_scale.contiguous() block_m, block_n, block_k, num_warps, num_stages = _select_triton_int8_config(m, k, n, device=a_int8.device, kernel_kind="scaled") grid = (triton.cdiv(m, block_m), triton.cdiv(n, block_n)) _scaled_int8_gemm_kernel[grid]( a_int8_c, b_int8_c, a_scale_c, b_scale_c, out, m, n, k, a_int8_c.stride(0), a_int8_c.stride(1), b_int8_c.stride(0), b_int8_c.stride(1), out.stride(0), out.stride(1), block_m=block_m, block_n=block_n, block_k=block_k, num_warps=num_warps, num_stages=num_stages, ) return out