Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6f3351f
autotune: harden cache key + add restore_value (issue #770)
jhinpan Jul 1, 2026
50a95c2
Merge branch 'main' into feat/autotune-cache-key-restore
jhinpan Jul 2, 2026
bff0d76
autotune: trim verbose comments/docstrings in autotune.py
jhinpan Jul 2, 2026
1bc2ee7
autotune: two-track config + first real adopter (rmsnorm) (#770)
jhinpan Jul 1, 2026
c6610e0
Merge branch 'main' into feat/autotune-rmsnorm-adopt (sync to 0.2.3)
jhinpan Jul 6, 2026
a19f7e6
autotune: make waves_per_eu compile-hint actually take effect
jhinpan Jul 6, 2026
304d0bc
autotune: make maxnreg take effect + reject unroutable num_warps
jhinpan Jul 7, 2026
2cb96df
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 7, 2026
73c553d
autotune: address multi-model review (thread-safety, error handling, …
jhinpan Jul 7, 2026
a8b87e4
Merge branch 'main' into feat/autotune-rmsnorm-adopt (sync after #783…
jhinpan Jul 7, 2026
0e347bb
autotune: address round-2 review (narrow cache-hit, do_bench setup, r…
jhinpan Jul 7, 2026
dd2180f
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 7, 2026
1735782
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 7, 2026
3f92925
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 8, 2026
42ae697
autotune: black-format test_autotune.py
jhinpan Jul 9, 2026
2a1384e
autotune: consolidate occupancy to gpu.func attrs, drop dead opts= path
jhinpan Jul 9, 2026
0ccfc1c
autotune: support per-kernel occupancy hints; document multi-kernel l…
jhinpan Jul 9, 2026
a74d162
autotune: Config accepts per-kernel occupancy maps; canonical hint ca…
jhinpan Jul 9, 2026
e157be0
autotune: fix zero-search default fast-path miss + harden occupancy h…
jhinpan Jul 9, 2026
8a0ccdd
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 10, 2026
b9038b3
autotune: move occupancy lowering into the ROCm backend
jhinpan Jul 10, 2026
79874a7
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 13, 2026
173ab72
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 13, 2026
b377161
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 13, 2026
8fbae45
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 14, 2026
18ddf6a
refactor: clarify occupancy hint lowering
jhinpan Jul 14, 2026
4d4a5a1
docs: trim occupancy lowering comments
jhinpan Jul 14, 2026
b29c2e7
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 14, 2026
79d3ece
autotune: model waves per eu as a compile option
jhinpan Jul 15, 2026
1bd314d
Merge branch 'main' into feat/autotune-rmsnorm-adopt
jhinpan Jul 15, 2026
011dacf
compiler: define occupancy hint precedence
jhinpan Jul 15, 2026
c4237a4
autotune: make rmsnorm config tests arch-stable
jhinpan Jul 15, 2026
80d57f9
compiler: centralize compile hint semantics
jhinpan Jul 16, 2026
a6de6a2
autotune: specialize rmsnorm through direct JIT
jhinpan Jul 16, 2026
f011227
autotune: trim rmsnorm adopter to essential path
jhinpan Jul 16, 2026
81baca5
autotune: fold rmsnorm config into adopter
jhinpan Jul 16, 2026
ef63ee8
autotune: fix stream-aware tuning and test contracts
jhinpan Jul 16, 2026
154688c
compiler: align compile-hint helper naming
jhinpan Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions kernels/norm/rmsnorm_autotune.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 FlyDSL Project Contributors

"""Autotuned RMSNorm — the first real adopter of ``flydsl.autotune``.

RMSNorm bakes its structural knob (BLOCK_THREADS) at module-build time, so the
tuner rebuilds the module per config via ``autotune_builder`` (builder mode).
Normal runs use the ``get_default`` heuristic; ``FLYDSL_AUTOTUNE=1`` sweeps
``get_all_configs``.

rmsnorm_autotuned(input, gamma, output, M, dtype_str="bf16", stream=stream)
"""

from flydsl.autotune import autotune_builder
from kernels.norm.rmsnorm_config import get_all_configs, get_default
from kernels.norm.rmsnorm_kernel import build_rmsnorm_module


def _specialize(input_t, gamma, output, m_in, dtype_str="bf16", stream=None):
# Build/lookup axes; dtype_str must be here so bf16 vs f16 keys differ.
return {"N": int(input_t.shape[-1]), "dtype_str": dtype_str}


rmsnorm_autotuned = autotune_builder(
name="rmsnorm",
build=build_rmsnorm_module,
specialize=_specialize,
configs=get_all_configs,
default=get_default,
structural=("BLOCK_THREADS",),
)
87 changes: 87 additions & 0 deletions kernels/norm/rmsnorm_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 FlyDSL Project Contributors

"""Two-track tuning configs for RMSNorm (quack-style get_default + exhaustive):

- ``get_default`` — analytic BLOCK_THREADS, zero search (normal runs).
- ``get_all_configs`` — BLOCK_THREADS x waves_per_eu, swept under FLYDSL_AUTOTUNE=1.

VEC_WIDTH is not tuned (pinned to 128//elem_bits by the 128-bit buffer copy).
"""

from flydsl.autotune import Config
from kernels.norm.rmsnorm_kernel import SMALL_N_THRESHOLD

# Candidate block sizes. All are multiples of the warp size (64 on CDNA) and
# span both the <=256 (no known_block_size) and >256 (needs known_block_size)
# regimes so the tuner can trade occupancy against per-thread work.
_BLOCK_THREADS_CHOICES = (128, 256, 512, 1024)
_WAVES_PER_EU_CHOICES = (0, 1, 2) # 0 == leave to the compiler; nonzero lowers to the rocdl.waves_per_eu func attr


def _elem_bits(dtype_str: str) -> int:
return 32 if dtype_str == "f32" else 16


def get_default(N: int, dtype_str: str, arch: str = None) -> Config:
"""Analytic default — a solid BLOCK_THREADS without searching.

Heuristic: pick the smallest block whose vectorized tiles cover the row in a
handful of iterations, clamped to [128, 1024]. Wider rows want more threads;
narrow rows keep the block small to preserve occupancy.

For bf16/f16 the pick is then shrunk to the largest candidate whose tile
(``BLOCK_THREADS * VEC_WIDTH``) evenly divides N, so the zero-search default
actually hits the vectorized fast path (the kernel gates it on
``N % tile_cols == 0``) -- matching the divisibility filter get_all_configs
applies. Without this, a "tile-aware" block can still miss the fast path and
silently fall to the slow scalar loop for common N (e.g. bf16 N=5120 picks
256 whose tile 2048 does not divide 5120 -> scalar, while 128 would
vectorize). If nothing divides N, every block runs scalar anyway, so keep
the heuristic pick. f32 always uses the scalar loop, so no filter applies.
"""
vec_width = 128 // _elem_bits(dtype_str)
# Aim for ~2 vectorized tiles per row: block ≈ N / (2 * vec_width).
target = N // max(1, (2 * vec_width))
block = 128
for choice in _BLOCK_THREADS_CHOICES:
if choice <= max(128, target):
block = choice

if _elem_bits(dtype_str) <= 16:
dividing = [b for b in _BLOCK_THREADS_CHOICES if b <= block and N >= b * vec_width and N % (b * vec_width) == 0]
if dividing:
block = max(dividing)

return Config(BLOCK_THREADS=block)


def get_all_configs(N: int, dtype_str: str, arch: str = None):
"""Exhaustive search space: BLOCK_THREADS x waves_per_eu.

bf16/f16 take the vectorized fast path (gated on ``elem_bits <= 16`` in the
kernel), so only BLOCK_THREADS whose tile ``BLOCK_THREADS * VEC_WIDTH``
evenly divides the row are kept. f32 never takes that path -- it uses the
scalar loop, which strides by BLOCK_THREADS and handles any N -- so every
BLOCK_THREADS is a valid, distinct f32 candidate (no tile filter). Previously
f32 was dropped entirely and silently collapsed to the single default."""
# Small-N kernel ignores BLOCK_THREADS, so there's nothing to sweep.
if N <= SMALL_N_THRESHOLD:
return [get_default(N, dtype_str, arch)]

vectorized = _elem_bits(dtype_str) <= 16
vec_width = 128 // _elem_bits(dtype_str)
configs = []
for block in _BLOCK_THREADS_CHOICES:
if vectorized:
tile_cols = block * vec_width
# bf16/f16: keep only blocks that hit the vectorized fast path for N.
if N < tile_cols or N % tile_cols != 0:
continue
for wpe in _WAVES_PER_EU_CHOICES:
waves = None if wpe == 0 else wpe
configs.append(Config(waves_per_eu=waves, BLOCK_THREADS=block))
# Fall back to the heuristic default if nothing fit (e.g. an odd bf16 N).
if not configs:
configs.append(get_default(N, dtype_str, arch))
return configs
20 changes: 17 additions & 3 deletions kernels/norm/rmsnorm_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@

KERNEL_NAME = "rmsnorm"

# N at or below this routes to the small-N kernel, whose block geometry is
# derived analytically and ignores BLOCK_THREADS. Single source of truth so the
# autotune config space (rmsnorm_config) stays in sync.
SMALL_N_THRESHOLD = 2048


def _store_yscale(scale_copy_atom, yscale_div, index, val):
r = fx.make_rmem_tensor(1, fx.Float32)
Expand All @@ -67,20 +72,29 @@ def _quant_dtype_max(dtype_str: str) -> float:
raise ValueError(f"unsupported quant dtype: {dtype_str!r} (expected 'i8' or 'int8')")


def build_rmsnorm_module(N: int, dtype_str: str, store_rstd: bool = False, eps: float = EPS):
if N <= 2048:
def build_rmsnorm_module(
N: int, dtype_str: str, store_rstd: bool = False, eps: float = EPS, BLOCK_THREADS: int = BLOCK_THREADS
):
if N <= SMALL_N_THRESHOLD:
return _build_rmsnorm_large_m_small_n_module(N, dtype_str, store_rstd, eps)

arch = get_rocm_arch()
USE_HW_CVT_PK_BF16_F32 = (arch == "gfx950") or str(arch).startswith("gfx95")

# BLOCK_THREADS is the block size (threads per row-block). It is a build-time
# structural knob: it sizes the shared reduction storage, the vectorized
# tile stride, and the launch block dim, so it is baked into the module
# rather than passed as a jit Constexpr. Autotune (builder mode) rebuilds
# this module per candidate BLOCK_THREADS. `known_block_size` is required
# on AMDGPU once the block exceeds 256.
tile_cols = BLOCK_THREADS * VEC_WIDTH
RED_SLOTS = max(1, (BLOCK_THREADS + WARP_SIZE - 1) // WARP_SIZE)
elem_bits = 32 if dtype_str == "f32" else 16
_kernel_kwargs = {} if BLOCK_THREADS <= 256 else {"known_block_size": [BLOCK_THREADS, 1, 1]}

SharedStorage = _make_reduction_storage(RED_SLOTS)

@flyc.kernel
@flyc.kernel(**_kernel_kwargs)
def rmsnorm_kernel(
Input: fx.Tensor,
Gamma: fx.Tensor,
Expand Down
6 changes: 5 additions & 1 deletion python/flydsl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@

__version__ = "0.2.4"

from .autotune import Config as Config, autotune as autotune # noqa: E402
from .autotune import ( # noqa: E402
Config as Config,
autotune as autotune,
autotune_builder as autotune_builder,
)
Loading
Loading