Files
8b51380466 [feat]: FastH3 on single RTX GPUs (#1919)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
Co-authored-by: Aryan Kumar <aryank@Aryans-Mac-Studio.local>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:37:01 -07:00

85 lines
3.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Triton helpers for per-token x per-channel FP8 linears on GPUs whose rowwise ``_scaled_mm`` is slow.
On sm89 (RTX 4090 / L40S / RTX 6000 Ada) ``torch._scaled_mm`` with rowwise scales runs at
~70 TFLOPS, below bf16 (~160), while the per-tensor kernel reaches 220-305 TFLOPS. The
per-token x per-channel result is recovered exactly by running the per-tensor kernel with unit
scales and applying ``out[i, j] *= sx[i] * sw[j]`` in one pass over the output, which costs
5-10% of the GEMM instead of 2-4x.
"""
from __future__ import annotations
import functools
import torch
import triton
import triton.language as tl
FP8_MAX = 448.0
FP8_MIN_SCALE = 1.0 / (FP8_MAX * 512.0)
@functools.cache
def rowwise_scaled_mm_is_slow() -> bool:
"""Ada (sm89) has no fast rowwise-scaled FP8 GEMM in torch; Hopper and Blackwell do."""
return torch.cuda.is_available() and torch.cuda.get_device_capability() == (8, 9)
@triton.jit
def _quantize_rowwise_kernel(x_ptr, q_ptr, s_ptr, K, stride_x, stride_q, BLOCK_K: tl.constexpr):
row = tl.program_id(0).to(tl.int64)
x_row = x_ptr + row * stride_x
amax = tl.zeros((BLOCK_K, ), dtype=tl.float32)
for k in range(0, K, BLOCK_K):
cols = k + tl.arange(0, BLOCK_K)
amax = tl.maximum(amax, tl.abs(tl.load(x_row + cols, mask=cols < K, other=0.0).to(tl.float32)))
scale = tl.maximum(tl.max(amax, axis=0) / 448.0, 1.0 / (448.0 * 512.0))
tl.store(s_ptr + row, scale)
inv = 1.0 / scale
for k in range(0, K, BLOCK_K):
cols = k + tl.arange(0, BLOCK_K)
v = tl.load(x_row + cols, mask=cols < K, other=0.0).to(tl.float32) * inv
v = tl.minimum(tl.maximum(v, -448.0), 448.0)
tl.store(q_ptr + row * stride_q + cols, v.to(tl.float8e4nv), mask=cols < K)
def quantize_rowwise_fp8(x_2d: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Per-token FP8 quantization in one launch. Returns ``(x_fp8 [M, K], x_scale [M, 1] float32)``."""
x_2d = x_2d.contiguous()
M, K = x_2d.shape
q = torch.empty((M, K), device=x_2d.device, dtype=torch.float8_e4m3fn)
s = torch.empty((M, 1), device=x_2d.device, dtype=torch.float32)
if M:
_quantize_rowwise_kernel[(M, )](x_2d, q, s, K, x_2d.stride(0), q.stride(0), BLOCK_K=1024, num_warps=4)
return q, s
@triton.jit
def _scale_rows_cols_kernel(o_ptr, sx_ptr, sw_ptr, M, N, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr):
rows = tl.program_id(0).to(tl.int64) * BLOCK_M + tl.arange(0, BLOCK_M)
cols = tl.program_id(1) * BLOCK_N + tl.arange(0, BLOCK_N)
mask = (rows[:, None] < M) & (cols[None, :] < N)
# int64 offsets: a 78k-token fc_in output has 2.2e9 elements, past int32.
ptrs = o_ptr + rows[:, None] * N + cols[None, :]
v = tl.load(ptrs, mask=mask, other=0.0).to(tl.float32)
sx = tl.load(sx_ptr + rows, mask=rows < M, other=0.0)
sw = tl.load(sw_ptr + cols, mask=cols < N, other=0.0)
tl.store(ptrs, (v * sx[:, None] * sw[None, :]).to(o_ptr.dtype.element_ty), mask=mask)
def scaled_mm_token_channel(x_fp8: torch.Tensor, x_scale: torch.Tensor, w_fp8_t: torch.Tensor,
w_scale: torch.Tensor) -> torch.Tensor:
"""``(x_fp8 * x_scale) @ (w_fp8_t * w_scale)`` in bf16 via the fast per-tensor GEMM plus a scale epilogue.
The unit-scale GEMM output is at most 448^2 * K, far inside bf16 range, and its relative
precision is that of any bf16 output, so the epilogue loses nothing against rowwise scaling.
"""
one = torch.ones((), device=x_fp8.device, dtype=torch.float32)
out = torch._scaled_mm(x_fp8, w_fp8_t, scale_a=one, scale_b=one, out_dtype=torch.bfloat16)
if isinstance(out, tuple):
out = out[0]
M, N = out.shape
grid = (triton.cdiv(M, 64), triton.cdiv(N, 128))
_scale_rows_cols_kernel[grid](out, x_scale.reshape(-1), w_scale.reshape(-1), M, N, BLOCK_M=64, BLOCK_N=128)
return out