59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
422 lines
14 KiB
Python
422 lines
14 KiB
Python
# Copyright (c) 2026 LightSeek Foundation
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
"""Triton activation helper kernels."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import torch
|
|
from tokenspeed_kernel._triton import tl, triton
|
|
from tokenspeed_kernel.ops.gemm.fp8_utils import (
|
|
create_per_token_group_quant_fp8_output_scale,
|
|
)
|
|
|
|
__all__ = [
|
|
"fused_gate_sigmoid_mul_add",
|
|
"fused_swiglu_fp8_ue8m0",
|
|
"sigmoid_mul",
|
|
"silu_and_mul",
|
|
]
|
|
|
|
|
|
@triton.jit
|
|
def _fused_gate_sigmoid_mul_add_kernel(
|
|
hidden_states_ptr,
|
|
gate_weight_ptr,
|
|
shared_output_ptr,
|
|
final_ptr,
|
|
hidden_dim: tl.constexpr,
|
|
BLOCK: tl.constexpr,
|
|
):
|
|
token_id = tl.program_id(0).to(tl.int64)
|
|
row_offset = token_id * hidden_dim
|
|
|
|
# Phase 1: gate = dot(hidden_states[token_id], gate_weight)
|
|
# BLOCK >= hidden_dim so this loop is single-iteration (unrolled away).
|
|
acc = tl.zeros([BLOCK], dtype=tl.float32)
|
|
for k_offset in range(0, hidden_dim, BLOCK):
|
|
cols = k_offset + tl.arange(0, BLOCK)
|
|
mask = cols < hidden_dim
|
|
h = tl.load(hidden_states_ptr + row_offset + cols, mask=mask, other=0.0)
|
|
w = tl.load(gate_weight_ptr + cols, mask=mask, other=0.0)
|
|
acc += h.to(tl.float32) * w.to(tl.float32)
|
|
gate_val = tl.sigmoid(tl.sum(acc, axis=0))
|
|
|
|
# Phase 2: final[token_id] += gate_val * shared_output[token_id]
|
|
for n_offset in range(0, hidden_dim, BLOCK):
|
|
cols = n_offset + tl.arange(0, BLOCK)
|
|
mask = cols < hidden_dim
|
|
s = tl.load(shared_output_ptr + row_offset + cols, mask=mask)
|
|
f = tl.load(final_ptr + row_offset + cols, mask=mask)
|
|
out = f.to(tl.float32) + gate_val * s.to(tl.float32)
|
|
tl.store(final_ptr + row_offset + cols, out.to(f.dtype), mask=mask)
|
|
|
|
|
|
def fused_gate_sigmoid_mul_add(
|
|
hidden_states: torch.Tensor,
|
|
gate_weight: torch.Tensor,
|
|
shared_output: torch.Tensor,
|
|
final_hidden_states: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
"""Fused ``final_hidden_states += sigmoid(hidden_states @ gate_weight) * shared_output``.
|
|
|
|
Computes the gate dot-product (reduction over hidden_dim), applies sigmoid,
|
|
multiplies by ``shared_output``, and adds to ``final_hidden_states`` in-place.
|
|
|
|
Args:
|
|
hidden_states: ``[num_tokens, hidden_dim]`` contiguous input.
|
|
gate_weight: ``[hidden_dim]`` contiguous 1-D weight vector.
|
|
shared_output: ``[num_tokens, hidden_dim]`` contiguous shared expert output.
|
|
final_hidden_states: ``[num_tokens, hidden_dim]`` contiguous MoE output,
|
|
modified in-place.
|
|
|
|
Returns:
|
|
``final_hidden_states`` (same storage, mutated in-place).
|
|
"""
|
|
if hidden_states.ndim != 2:
|
|
raise ValueError(f"hidden_states must be 2D, got {hidden_states.ndim}D")
|
|
if not hidden_states.is_contiguous():
|
|
raise ValueError("hidden_states must be contiguous")
|
|
if gate_weight.ndim != 1:
|
|
raise ValueError(f"gate_weight must be 1D, got {gate_weight.ndim}D")
|
|
if not gate_weight.is_contiguous():
|
|
raise ValueError("gate_weight must be contiguous")
|
|
if not shared_output.is_contiguous():
|
|
raise ValueError("shared_output must be contiguous")
|
|
if not final_hidden_states.is_contiguous():
|
|
raise ValueError("final_hidden_states must be contiguous")
|
|
|
|
num_tokens, hidden_dim = hidden_states.shape
|
|
if gate_weight.shape[0] != hidden_dim:
|
|
raise ValueError(
|
|
f"gate_weight dim mismatch: expected {hidden_dim}, got {gate_weight.shape[0]}"
|
|
)
|
|
if shared_output.shape != (num_tokens, hidden_dim):
|
|
raise ValueError(
|
|
f"shared_output shape mismatch: expected {(num_tokens, hidden_dim)}, "
|
|
f"got {shared_output.shape}"
|
|
)
|
|
if final_hidden_states.shape != (num_tokens, hidden_dim):
|
|
raise ValueError(
|
|
f"final_hidden_states shape mismatch: expected {(num_tokens, hidden_dim)}, "
|
|
f"got {final_hidden_states.shape}"
|
|
)
|
|
|
|
if num_tokens == 0:
|
|
return final_hidden_states
|
|
|
|
BLOCK = triton.next_power_of_2(hidden_dim)
|
|
num_warps = 4 if BLOCK <= 2048 else (8 if BLOCK <= 4096 else 16)
|
|
grid = (num_tokens,)
|
|
_fused_gate_sigmoid_mul_add_kernel[grid](
|
|
hidden_states,
|
|
gate_weight,
|
|
shared_output,
|
|
final_hidden_states,
|
|
hidden_dim=hidden_dim,
|
|
BLOCK=BLOCK,
|
|
num_warps=num_warps,
|
|
)
|
|
return final_hidden_states
|
|
|
|
|
|
@triton.jit
|
|
def _sigmoid_mul_kernel(
|
|
x_ptr,
|
|
gate_ptr,
|
|
n_elements,
|
|
hidden_dim: tl.constexpr,
|
|
head_dim: tl.constexpr,
|
|
gate_row_stride: tl.constexpr,
|
|
gate_head_stride: tl.constexpr,
|
|
BLOCK_SIZE: tl.constexpr,
|
|
):
|
|
pid = tl.program_id(0).to(tl.int64)
|
|
block_start = pid * BLOCK_SIZE
|
|
offsets = block_start + tl.arange(0, BLOCK_SIZE)
|
|
mask = offsets < n_elements
|
|
|
|
row = offsets // hidden_dim
|
|
col = offsets % hidden_dim
|
|
head = col // head_dim
|
|
d = col % head_dim
|
|
gate_addrs = gate_ptr + row * gate_row_stride + head * gate_head_stride + d
|
|
|
|
x = tl.load(x_ptr + offsets, mask=mask).to(tl.float32)
|
|
g = tl.load(gate_addrs, mask=mask).to(tl.float32)
|
|
out = x * tl.sigmoid(g)
|
|
tl.store(x_ptr + offsets, out, mask=mask)
|
|
|
|
|
|
def sigmoid_mul(x: torch.Tensor, gate: torch.Tensor) -> torch.Tensor:
|
|
"""In-place ``x *= sigmoid(gate)``.
|
|
|
|
``x`` must be contiguous 2D ``[num_tokens, hidden_dim]`` and is mutated.
|
|
``gate`` may be either
|
|
|
|
- 2D contiguous ``[num_tokens, hidden_dim]``, or
|
|
- 3D ``[num_tokens, num_heads, head_dim]`` with ``stride(-1) == 1`` —
|
|
the strided view that ``torch.chunk(q_gate, 2, dim=-1)`` produces from
|
|
a packed ``[num_tokens, num_heads, 2 * head_dim]`` tensor.
|
|
|
|
The strided form lets callers skip the ``.reshape(-1)`` copy after the
|
|
chunk; both layouts share the same kernel via the explicit gate strides.
|
|
"""
|
|
if x.ndim != 2:
|
|
raise ValueError(f"x must be 2D, got {x.ndim}D")
|
|
if not x.is_contiguous():
|
|
raise ValueError("x must be contiguous")
|
|
if gate.stride(-1) != 1:
|
|
raise ValueError(f"gate must have stride(-1) == 1, got {gate.stride()}")
|
|
if x.dtype != gate.dtype:
|
|
raise ValueError(f"dtype mismatch: x={x.dtype} gate={gate.dtype}")
|
|
|
|
num_tokens, hidden_dim = x.shape
|
|
|
|
if gate.ndim == 2:
|
|
if gate.shape != x.shape:
|
|
raise ValueError(f"shape mismatch: x={x.shape} gate={gate.shape}")
|
|
head_dim = hidden_dim
|
|
gate_row_stride = gate.stride(0)
|
|
gate_head_stride = hidden_dim
|
|
elif gate.ndim == 3:
|
|
gate_tokens, num_heads, head_dim = gate.shape
|
|
if gate_tokens != num_tokens:
|
|
raise ValueError(f"num_tokens mismatch: x={num_tokens} gate={gate_tokens}")
|
|
if num_heads * head_dim != hidden_dim:
|
|
raise ValueError(
|
|
f"hidden_dim mismatch: x={hidden_dim} gate={num_heads}*{head_dim}"
|
|
)
|
|
gate_row_stride = gate.stride(0)
|
|
gate_head_stride = gate.stride(1)
|
|
else:
|
|
raise ValueError(f"gate must be 2D or 3D, got {gate.ndim}D")
|
|
|
|
n = x.numel()
|
|
if n == 0:
|
|
return x
|
|
|
|
BLOCK_SIZE = 1024
|
|
grid = ((n + BLOCK_SIZE - 1) // BLOCK_SIZE,)
|
|
_sigmoid_mul_kernel[grid](
|
|
x,
|
|
gate,
|
|
n,
|
|
hidden_dim=hidden_dim,
|
|
head_dim=head_dim,
|
|
gate_row_stride=gate_row_stride,
|
|
gate_head_stride=gate_head_stride,
|
|
BLOCK_SIZE=BLOCK_SIZE,
|
|
)
|
|
return x
|
|
|
|
|
|
@triton.jit
|
|
def _silu_and_mul_kernel(
|
|
x_ptr,
|
|
out_ptr,
|
|
n_elements,
|
|
hidden_dim: tl.constexpr,
|
|
input_stride_row: tl.constexpr,
|
|
out_stride_row: tl.constexpr,
|
|
BLOCK_SIZE: tl.constexpr,
|
|
):
|
|
pid = tl.program_id(0).to(tl.int64)
|
|
block_start = pid * BLOCK_SIZE
|
|
offsets = block_start + tl.arange(0, BLOCK_SIZE)
|
|
mask = offsets < n_elements
|
|
|
|
row = offsets // hidden_dim
|
|
col = offsets % hidden_dim
|
|
gate_addrs = x_ptr + row * input_stride_row + col
|
|
up_addrs = gate_addrs + hidden_dim
|
|
|
|
gate = tl.load(gate_addrs, mask=mask).to(tl.float32)
|
|
up = tl.load(up_addrs, mask=mask).to(tl.float32)
|
|
out = gate * tl.sigmoid(gate) * up
|
|
tl.store(out_ptr + row * out_stride_row + col, out, mask=mask)
|
|
|
|
|
|
def silu_and_mul(
|
|
x: torch.Tensor,
|
|
out: torch.Tensor | None = None,
|
|
*,
|
|
enable_pdl: bool = False,
|
|
) -> torch.Tensor:
|
|
"""Fused ``SiLU(x[..., :D]) * x[..., D:]``.
|
|
|
|
``x`` is interpreted as ``[..., 2 * D]`` with gate values in the first half
|
|
and up values in the second half. The output has shape ``[..., D]``.
|
|
"""
|
|
del enable_pdl
|
|
if x.shape[-1] % 2 != 0:
|
|
raise ValueError(f"last dimension must be even, got {x.shape[-1]}")
|
|
if x.stride(-1) != 1:
|
|
x = x.contiguous()
|
|
|
|
hidden_dim = x.shape[-1] // 2
|
|
output_shape = (*x.shape[:-1], hidden_dim)
|
|
if out is None:
|
|
out = torch.empty(output_shape, dtype=x.dtype, device=x.device)
|
|
elif tuple(out.shape) != output_shape:
|
|
raise ValueError(f"out shape must be {output_shape}, got {tuple(out.shape)}")
|
|
|
|
if out.stride(-1) != 1:
|
|
raise ValueError("out must have stride(-1) == 1")
|
|
|
|
flat_x = x.reshape(-1, x.shape[-1])
|
|
flat_out = out.reshape(-1, hidden_dim)
|
|
n = flat_out.numel()
|
|
if n == 0:
|
|
return out
|
|
|
|
BLOCK_SIZE = 1024
|
|
grid = ((n + BLOCK_SIZE - 1) // BLOCK_SIZE,)
|
|
_silu_and_mul_kernel[grid](
|
|
flat_x,
|
|
flat_out,
|
|
n,
|
|
hidden_dim=hidden_dim,
|
|
input_stride_row=flat_x.stride(0),
|
|
out_stride_row=flat_out.stride(0),
|
|
BLOCK_SIZE=BLOCK_SIZE,
|
|
)
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fused SwiGLU + FP8 UE8M0 quantization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@triton.jit
|
|
def _fused_swiglu_fp8_ue8m0_kernel(
|
|
gate_up_ptr,
|
|
out_ptr,
|
|
scale_ptr,
|
|
M,
|
|
N: tl.constexpr,
|
|
gate_up_stride_row,
|
|
out_stride_row,
|
|
scale_col_stride,
|
|
swiglu_limit,
|
|
eps,
|
|
bit8_min,
|
|
bit8_max,
|
|
GROUP_SIZE: tl.constexpr,
|
|
):
|
|
pid = tl.program_id(0)
|
|
groups_per_row = N // GROUP_SIZE
|
|
row = pid // groups_per_row
|
|
group_col = pid % groups_per_row
|
|
|
|
gate_offset = (
|
|
row.to(tl.int64) * gate_up_stride_row + group_col.to(tl.int64) * GROUP_SIZE
|
|
)
|
|
up_offset = gate_offset + N
|
|
out_offset = row.to(tl.int64) * out_stride_row + group_col.to(tl.int64) * GROUP_SIZE
|
|
|
|
cols = tl.arange(0, GROUP_SIZE)
|
|
|
|
gate = tl.load(gate_up_ptr + gate_offset + cols).to(tl.float32)
|
|
up = tl.load(gate_up_ptr + up_offset + cols).to(tl.float32)
|
|
|
|
if swiglu_limit > 0.0:
|
|
gate = tl.minimum(gate, swiglu_limit)
|
|
up = tl.clamp(up, -swiglu_limit, swiglu_limit)
|
|
|
|
silu_gate = gate * tl.sigmoid(gate)
|
|
y = silu_gate * up
|
|
|
|
_absmax = tl.max(tl.abs(y))
|
|
scale_raw = tl.maximum(_absmax / bit8_max, eps)
|
|
exponent = tl.ceil(tl.log2(scale_raw))
|
|
y_s = tl.exp2(exponent)
|
|
y_q = tl.clamp(y / y_s, bit8_min, bit8_max).to(out_ptr.dtype.element_ty)
|
|
|
|
tl.store(out_ptr + out_offset + cols, y_q)
|
|
|
|
scale_pack_col = group_col // 4
|
|
scale_pack_pos = group_col % 4
|
|
scale_ptr_offset = scale_pack_col.to(tl.int64) * scale_col_stride + row.to(tl.int64)
|
|
exponent_biased = tl.clamp(exponent + 127.0, 0.0, 255.0).to(tl.uint32)
|
|
packed_scale = exponent_biased << (scale_pack_pos * 8)
|
|
tl.atomic_or(scale_ptr + scale_ptr_offset, packed_scale, sem="relaxed")
|
|
|
|
|
|
def fused_swiglu_fp8_ue8m0(
|
|
gate_up: torch.Tensor,
|
|
swiglu_limit: float = 0.0,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Fused SwiGLU activation + FP8 UE8M0 block-scale quantization.
|
|
|
|
Reads a ``[M, 2*N]`` gate_up tensor (gate in the first half, up in the
|
|
second half), applies ``clamp + SiLU(gate) * up``, and quantizes the
|
|
result to FP8 E4M3 with UE8M0 packed block scales in one kernel pass.
|
|
|
|
Args:
|
|
gate_up: ``[M, 2*N]`` tensor (BF16 or FP8; cast to float32 internally).
|
|
swiglu_limit: Clamp bound. 0 or negative disables clamping.
|
|
|
|
Returns:
|
|
``(fp8_out, scale)``: ``fp8_out`` is ``[M, N]`` float8_e4m3fn,
|
|
``scale`` is UE8M0 packed int32 column-major TMA-aligned.
|
|
"""
|
|
assert gate_up.ndim == 2, f"Expected 2D input, got {gate_up.ndim}D"
|
|
M, two_N = gate_up.shape
|
|
assert two_N % 2 == 0
|
|
N = two_N // 2
|
|
assert N % 128 == 0, f"N={N} must be multiple of 128 for UE8M0 group_size=128"
|
|
|
|
GROUP_SIZE = 128
|
|
dtype = torch.float8_e4m3fn
|
|
info = torch.finfo(dtype)
|
|
|
|
out = torch.empty((M, N), device=gate_up.device, dtype=dtype)
|
|
scale = create_per_token_group_quant_fp8_output_scale(
|
|
x_shape=(M, N),
|
|
device=gate_up.device,
|
|
group_size=GROUP_SIZE,
|
|
column_major_scales=True,
|
|
scale_tma_aligned=True,
|
|
scale_ue8m0=True,
|
|
)
|
|
|
|
num_groups = M * (N // GROUP_SIZE)
|
|
_fused_swiglu_fp8_ue8m0_kernel[(num_groups,)](
|
|
gate_up,
|
|
out,
|
|
scale,
|
|
M,
|
|
N,
|
|
gate_up.stride(0),
|
|
out.stride(0),
|
|
scale.stride(-1),
|
|
swiglu_limit if swiglu_limit is not None and swiglu_limit > 0 else 0.0,
|
|
1e-10,
|
|
bit8_min=info.min,
|
|
bit8_max=info.max,
|
|
GROUP_SIZE=GROUP_SIZE,
|
|
num_warps=min(max(GROUP_SIZE // 256, 1), 8),
|
|
num_stages=1,
|
|
)
|
|
|
|
return out, scale
|