-
Notifications
You must be signed in to change notification settings - Fork 59
[WS1][kernels] RoPE (Triton, CUDA on SM90) #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # Copyright (c) 2026 RL-Kernel Contributors | ||
|
|
||
| """Benchmark NativeRoPEOp vs TritonRoPEOp vs RoPESM90Op. | ||
|
|
||
| RoPE is an elementwise per-position rotation. The native path builds broadcast | ||
| cos/sin caches and does the rotate-half in PyTorch; the Triton and CUDA (SM90) | ||
| kernels fuse the rotation and round back to the input dtype on store, so they | ||
| touch less memory and are faster. Latency (forward and forward+backward) and peak | ||
| forward VRAM are reported, swept over (batch, seq) on the Qwen3-8B rotary config | ||
| (n_heads=32, head_dim=128, theta=1e6). | ||
|
|
||
| Usage: | ||
| python benchmarks/benchmark_rope.py | ||
| python benchmarks/benchmark_rope.py --configs "8,512;16,4096" | ||
| """ | ||
|
|
||
| import argparse | ||
|
|
||
| import torch | ||
| from tabulate import tabulate | ||
|
|
||
| from rl_engine.kernels.ops.pytorch.rotary_embedding.rope import NativeRoPEOp | ||
| from rl_engine.kernels.ops.triton.rotary_embedding.rope import TritonRoPEOp | ||
| from rl_engine.platforms.device import device_ctx | ||
| from rl_engine.utils.logger import logger | ||
|
|
||
| # Qwen3-8B rotary config. | ||
| N_HEADS = 32 | ||
| HEAD_DIM = 128 | ||
| THETA = 1.0e6 | ||
|
|
||
|
|
||
| def _maybe_sm90_op(): | ||
| """The Hopper (SM90) RoPE op, or None when unavailable (non-Hopper / not built).""" | ||
| from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE | ||
|
|
||
| if not ( | ||
| torch.cuda.is_available() | ||
| and torch.cuda.get_device_capability()[0] == 9 | ||
| and _EXT_AVAILABLE | ||
| and hasattr(_C, "rope_apply_sm90") | ||
| ): | ||
| return None | ||
| from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op | ||
|
|
||
| return RoPESM90Op() | ||
|
|
||
|
|
||
| # (batch, seq) | ||
| DEFAULT_CONFIGS = [ | ||
| (8, 512), | ||
| (8, 2048), | ||
| (16, 4096), | ||
| (8, 8192), | ||
| ] | ||
|
|
||
|
|
||
| def _make_inputs(batch, seq, device, dtype): | ||
| x = torch.randn(batch, N_HEADS, seq, HEAD_DIM, device=device, dtype=dtype) | ||
| positions = torch.arange(seq, device=device, dtype=torch.long) | ||
| return x, positions | ||
|
|
||
|
|
||
| def _time_ms(fn, warmup, iters): | ||
| for _ in range(warmup): | ||
| fn() | ||
| torch.cuda.synchronize() | ||
| start = torch.cuda.Event(enable_timing=True) | ||
| end = torch.cuda.Event(enable_timing=True) | ||
| start.record() | ||
| for _ in range(iters): | ||
| fn() | ||
| end.record() | ||
| torch.cuda.synchronize() | ||
| return start.elapsed_time(end) / iters | ||
|
|
||
|
|
||
| def _peak_vram_gb(fn, warmup=3, iters=5): | ||
| for _ in range(warmup): | ||
| fn() | ||
| torch.cuda.synchronize() | ||
| torch.cuda.empty_cache() | ||
| baseline = torch.cuda.memory_allocated() | ||
| torch.cuda.reset_peak_memory_stats() | ||
| for _ in range(iters): | ||
| fn() | ||
| torch.cuda.synchronize() | ||
| return (torch.cuda.max_memory_allocated() - baseline) / (1024**3) | ||
|
|
||
|
|
||
| def run_benchmark(args): | ||
| if device_ctx.device_type not in ["cuda", "xpu", "hip"]: | ||
| raise RuntimeError("rope benchmark requires a compatible GPU device.") | ||
|
|
||
| device = device_ctx.device | ||
| dtype = torch.bfloat16 | ||
| native = NativeRoPEOp() | ||
| triton_op = TritonRoPEOp() | ||
| sm90_op = _maybe_sm90_op() | ||
|
|
||
| logger.info( | ||
| f"rope benchmark on {device} (dtype={dtype}); " | ||
| f"SM90 backend {'enabled' if sm90_op is not None else 'unavailable'}" | ||
| ) | ||
|
|
||
| rows = [] | ||
| for batch, seq in args.configs: | ||
| x, positions = _make_inputs(batch, seq, device, dtype) | ||
|
|
||
| def fwd(op, x=x, pos=positions): | ||
| with torch.no_grad(): | ||
| op(x, pos, theta=THETA) | ||
|
|
||
| def fwd_bwd(op, x_src=x, pos=positions): | ||
| x_in = x_src.clone().requires_grad_(True) | ||
| op(x_in, pos, theta=THETA).sum().backward() | ||
|
Comment on lines
+115
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Map the benchmark file and inspect the relevant section with line numbers.
wc -l benchmarks/benchmark_rope.py
sed -n '1,220p' benchmarks/benchmark_rope.py | cat -n
# Show only the RoPE benchmark function definitions / call sites.
rg -n "fwd_bwd|clone\(\)|sum\(\)|backward\(|theta=THETA|positions" benchmarks/benchmark_rope.pyRepository: RL-Align/RL-Kernel Length of output: 7971 🏁 Script executed: #!/bin/bash
set -euo pipefail
# If the file is longer than expected, inspect only the relevant window around lines 100-140.
sed -n '100,140p' benchmarks/benchmark_rope.py | cat -nRepository: RL-Align/RL-Kernel Length of output: 1973 Exclude clone/reduction from the RoPE fwd+bwd timer. 🤖 Prompt for AI Agents |
||
|
|
||
| n_fwd = _time_ms(lambda: fwd(native), args.warmup, args.iters) | ||
| t_fwd = _time_ms(lambda: fwd(triton_op), args.warmup, args.iters) | ||
| n_fb = _time_ms(lambda: fwd_bwd(native), args.warmup, args.iters) | ||
| t_fb = _time_ms(lambda: fwd_bwd(triton_op), args.warmup, args.iters) | ||
| n_vram = _peak_vram_gb(lambda: fwd(native)) | ||
| t_vram = _peak_vram_gb(lambda: fwd(triton_op)) | ||
|
|
||
| row = [ | ||
| f"{batch}x{seq}", | ||
| f"{n_fwd:.3f}", | ||
| f"{t_fwd:.3f}", | ||
| f"{n_fwd/t_fwd:.2f}x", | ||
| f"{n_fb:.3f}", | ||
| f"{t_fb:.3f}", | ||
| f"{n_fb/t_fb:.2f}x", | ||
| f"{n_vram*1024:.0f}", | ||
| f"{t_vram*1024:.0f}", | ||
| ] | ||
| if sm90_op is not None: | ||
| s_fwd = _time_ms(lambda: fwd(sm90_op), args.warmup, args.iters) | ||
| s_fb = _time_ms(lambda: fwd_bwd(sm90_op), args.warmup, args.iters) | ||
| s_vram = _peak_vram_gb(lambda: fwd(sm90_op)) | ||
| row += [ | ||
| f"{s_fwd:.3f}", | ||
| f"{n_fwd/s_fwd:.2f}x", | ||
| f"{t_fwd/s_fwd:.2f}x", | ||
| f"{s_fb:.3f}", | ||
| f"{s_vram*1024:.0f}", | ||
| ] | ||
| rows.append(row) | ||
|
|
||
| headers = [ | ||
| "shape (B x S)", | ||
| "native fwd ms", | ||
| "triton fwd ms", | ||
| "fwd speedup", | ||
| "native f+b ms", | ||
| "triton f+b ms", | ||
| "f+b speedup", | ||
| "native fwd MB", | ||
| "triton fwd MB", | ||
| ] | ||
| if sm90_op is not None: | ||
| headers += [ | ||
| "sm90 fwd ms", | ||
| "sm90 vs native", | ||
| "sm90 vs triton", | ||
| "sm90 f+b ms", | ||
| "sm90 fwd MB", | ||
| ] | ||
| print(tabulate(rows, headers=headers, tablefmt="github")) | ||
|
|
||
|
|
||
| def parse_args(): | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("--iters", type=int, default=20) | ||
| parser.add_argument("--warmup", type=int, default=5) | ||
| parser.add_argument( | ||
| "--configs", | ||
| type=str, | ||
| default=None, | ||
| help="Semicolon-separated 'batch,seq' tuples, e.g. '8,512;16,4096'.", | ||
| ) | ||
| args = parser.parse_args() | ||
| if args.configs: | ||
| args.configs = [tuple(int(x) for x in tup.split(",")) for tup in args.configs.split(";")] | ||
| else: | ||
| args.configs = DEFAULT_CONFIGS | ||
| return args | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run_benchmark(parse_args()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright (c) 2026 RL-Kernel Contributors | ||
| // | ||
| // CUDA RoPE kernel for SM90 (GPT-NeoX / HF rotate-half), matching NativeRoPEOp. | ||
| // | ||
| // For a row (one [B, H, S] token vector of width D, half = D / 2) at sequence | ||
| // index s = row % S and pair index i in [0, half): | ||
| // | ||
| // c = cos[s, i] (fp32, precomputed to match the reference math) | ||
| // sn = sin[s, i] * sin_sign (sin_sign = +1 forward, -1 backward) | ||
| // out[i] = x[i] * c - x[i + half] * sn | ||
| // out[i + half] = x[i + half] * c + x[i] * sn | ||
| // | ||
| // The elementwise rotation is done in fp32 and rounded back to the input dtype. | ||
| // Backward reuses the same kernel with sin_sign = -1 (RoPE is an orthogonal | ||
| // per-position rotation, so grad_x = grad_out * cos - rotate_half(grad_out) * sin). | ||
|
|
||
| #include <torch/extension.h> | ||
| #include <ATen/cuda/CUDAContext.h> | ||
| #include <c10/cuda/CUDAGuard.h> | ||
|
|
||
| namespace { | ||
|
|
||
| template <typename scalar_t> | ||
| __global__ void rope_apply_sm90_kernel( | ||
| const scalar_t* __restrict__ x, // [n_rows, D] | ||
| const float* __restrict__ cos, // [S, half] | ||
| const float* __restrict__ sin, // [S, half] | ||
| scalar_t* __restrict__ out, // [n_rows, D] | ||
| const int64_t n_rows, | ||
| const int S, | ||
| const int half, | ||
| const float sin_sign) { | ||
| const int64_t idx = blockIdx.x * static_cast<int64_t>(blockDim.x) + threadIdx.x; | ||
| const int64_t total = n_rows * static_cast<int64_t>(half); | ||
| if (idx >= total) { | ||
| return; | ||
| } | ||
|
|
||
| const int64_t row = idx / half; | ||
| const int i = static_cast<int>(idx % half); | ||
| const int seq = static_cast<int>(row % S); | ||
|
|
||
| const float c = cos[seq * half + i]; | ||
| const float sn = sin[seq * half + i] * sin_sign; | ||
|
|
||
| const int64_t base = row * (2LL * half); | ||
| const float x1 = static_cast<float>(x[base + i]); | ||
| const float x2 = static_cast<float>(x[base + i + half]); | ||
|
|
||
| out[base + i] = static_cast<scalar_t>(x1 * c - x2 * sn); | ||
| out[base + i + half] = static_cast<scalar_t>(x2 * c + x1 * sn); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| // x: [n_rows, D] contiguous (any float dtype); cos/sin: [S, half] fp32 contiguous. | ||
| torch::Tensor rope_apply_sm90( | ||
| torch::Tensor x, | ||
| torch::Tensor cos, | ||
| torch::Tensor sin, | ||
| double sin_sign) { | ||
| TORCH_CHECK(x.is_cuda(), "rope: x must be a CUDA tensor"); | ||
| TORCH_CHECK(x.dim() == 2, "rope: x must be 2-D [n_rows, D]"); | ||
| TORCH_CHECK(x.is_contiguous(), "rope: x must be contiguous"); | ||
| TORCH_CHECK(cos.is_cuda() && sin.is_cuda(), "rope: cos/sin must be CUDA tensors"); | ||
| TORCH_CHECK(cos.scalar_type() == torch::kFloat32 && sin.scalar_type() == torch::kFloat32, | ||
| "rope: cos/sin must be fp32"); | ||
| TORCH_CHECK(cos.is_contiguous() && sin.is_contiguous(), "rope: cos/sin must be contiguous"); | ||
|
|
||
| const int64_t n_rows = x.size(0); | ||
| const int64_t D = x.size(1); | ||
| TORCH_CHECK(D % 2 == 0, "rope: head_dim must be even"); | ||
| const int half = static_cast<int>(D / 2); | ||
| const int S = static_cast<int>(cos.size(0)); | ||
| TORCH_CHECK(cos.size(1) == half && sin.size(1) == half, | ||
| "rope: cos/sin last dim must equal head_dim/2"); | ||
| TORCH_CHECK(S > 0 && n_rows % S == 0, | ||
| "rope: n_rows must be divisible by seq length S"); | ||
|
|
||
| const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); | ||
| auto out = torch::empty_like(x); | ||
|
|
||
| const int64_t total = n_rows * static_cast<int64_t>(half); | ||
| const int threads = 256; | ||
| const int64_t blocks = (total + threads - 1) / threads; | ||
| auto stream = at::cuda::getCurrentCUDAStream(); | ||
|
|
||
| AT_DISPATCH_FLOATING_TYPES_AND2( | ||
| at::ScalarType::Half, at::ScalarType::BFloat16, x.scalar_type(), "rope_apply_sm90", [&] { | ||
| rope_apply_sm90_kernel<scalar_t><<<blocks, threads, 0, stream>>>( | ||
| x.data_ptr<scalar_t>(), | ||
| cos.data_ptr<float>(), | ||
| sin.data_ptr<float>(), | ||
| out.data_ptr<scalar_t>(), | ||
| n_rows, | ||
| S, | ||
| half, | ||
| static_cast<float>(sin_sign)); | ||
| }); | ||
| return out; | ||
|
Comment on lines
+89
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Locate the established launch-check macro and required includes.
rg -n -C3 'C10_CUDA_KERNEL_LAUNCH_CHECK|AT_CUDA_CHECK|cudaGetLastError' csrcRepository: RL-Align/RL-Kernel Length of output: 1726 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== rope_sm90.cu =="
nl -ba csrc/cuda/rope_sm90.cu | sed -n '1,180p'
echo
echo "== nearby CUDA kernel launch patterns =="
rg -n -C2 'C10_CUDA_KERNEL_LAUNCH_CHECK|CUDA_CHECK\\(cudaGetLastError\\)|<<<' csrc/cuda csrc | sed -n '1,220p'Repository: RL-Align/RL-Kernel Length of output: 215 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== rope_sm90.cu outline =="
ast-grep outline csrc/cuda/rope_sm90.cu --view expanded || trueRepository: RL-Align/RL-Kernel Length of output: 285 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path("csrc/cuda/rope_sm90.cu")
print(p.exists(), p.stat().st_size if p.exists() else None)
PYRepository: RL-Align/RL-Kernel Length of output: 166 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== csrc/cuda/rope_sm90.cu (1-220) =="
sed -n '1,220p' csrc/cuda/rope_sm90.cu
echo
echo "== launch-check symbols in rope_sm90.cu =="
rg -n 'C10_CUDA_KERNEL_LAUNCH_CHECK|CUDA_CHECK\\(cudaGetLastError\\)|cudaGetLastError|AT_CUDA_CHECK' csrc/cuda/rope_sm90.cu || trueRepository: RL-Align/RL-Kernel Length of output: 4137 Check the CUDA launch here. 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # Copyright (c) 2026 RL-Kernel Contributors | ||
|
|
||
| from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op | ||
|
|
||
| __all__ = ["RoPESM90Op"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: RL-Align/RL-Kernel
Length of output: 29828
Restrict this benchmark to CUDA/HIP devices
At
benchmarks/benchmark_rope.py:93-94, allowingxpuhere is incompatible with the unconditionaltorch.cuda.*sync/event/memory calls, so XPU runs will fail or report invalid metrics. Use device-specific helpers or drop XPU support from this benchmark.🤖 Prompt for AI Agents