Automated PR - 2026-07-07

This commit is contained in:
github-actions[bot]
2026-07-07 16:57:50 +00:00
parent 780984275f
commit 63fd9a4f86
157 changed files with 15976 additions and 5043 deletions
@@ -0,0 +1,6 @@
"""ltx-kernels: High-performance CUDA kernels for distributed attention operations."""
from ltx_kernels.all_to_all import All2All
__all__ = ["All2All"]
__version__ = "0.1.0"
@@ -0,0 +1,183 @@
"""All2All communication primitives for distributed attention.
The C++ kernels are exposed via ``torch.library.custom_op`` so that
``torch.compile`` (and CUDA Graph capture under ``mode="reduce-overhead"``)
can trace through them without a graph break. Each :class:`All2All`
instance registers itself in a class-level registry indexed by an integer
``comm_id``; the custom op takes that id plus an input tensor and dispatches
to the appropriate C++ runtime.
"""
import math
import weakref
from typing import Any, ClassVar
import torch
import torch.distributed as dist
from all2all_cpp import All2All as All2AllCpp
from torch.library import custom_op
# Output shapes are derived symbolically from the input tensor's shape under
# the assumption of *uniform* sharding (caller pads up-front so
# `total_tokens % world_size == 0`). `world_size` travels through the op as an int
# so it enters the traced graph: the fake needs it for the output shape, and since
# it is constant per process the Dynamo guard it installs never triggers a
# within-run recompile -- it only keys the compile cache by world size, so a graph
# compiled under one GPU count is never replayed under another. Per-rank token
# counts, which do vary per call, stay in the C++ runtime state (`set_rank_tokens`)
# and never travel through the op.
@custom_op("ltx_kernels::send_recv_heads", mutates_args=(), device_types="cuda")
def _send_recv_heads_op(
x: torch.Tensor,
comm_id: int,
world_size: int, # noqa: ARG001
copy_out: bool,
) -> torch.Tensor:
# copy_out=False returns a view into the IPC buffer (zero-copy). Safe under
# cudagraph_trees because the IPC buffer is cudaMalloc'd inside All2All, not
# in the static graph pool, so it isn't subject to graph-pool aliasing.
return All2All._runtime_registry[comm_id].send_recv_heads(x, copy_out)
@_send_recv_heads_op.register_fake
def _send_recv_heads_fake(
x: torch.Tensor,
comm_id: int, # noqa: ARG001
world_size: int,
copy_out: bool, # noqa: ARG001
) -> torch.Tensor:
return x.new_empty((x.shape[0], x.shape[1] * world_size, x.shape[2] // world_size, x.shape[3]))
@custom_op("ltx_kernels::gather_heads", mutates_args=(), device_types="cuda")
def _gather_heads_op(
x: torch.Tensor,
comm_id: int,
world_size: int, # noqa: ARG001
copy_out: bool,
) -> torch.Tensor:
return All2All._runtime_registry[comm_id].gather_heads(x, copy_out)
@_gather_heads_op.register_fake
def _gather_heads_fake(
x: torch.Tensor,
comm_id: int, # noqa: ARG001
world_size: int,
copy_out: bool, # noqa: ARG001
) -> torch.Tensor:
return x.new_empty((x.shape[0], x.shape[1] // world_size, x.shape[2] * world_size, x.shape[3]))
class All2All:
"""IPC-based All2All communication for distributed head-parallel attention.
This class manages GPU memory buffers and IPC handles to enable efficient
cross-GPU communication for attention head redistribution.
Args:
rank: Local rank of this process.
world_size: Total number of processes in the distributed group.
seqlen: Maximum sequence length to allocate buffers for.
hidden_dim: Hidden dimension size (num_heads * head_dim).
num_sms: Number of SMs to use for kernel execution.
tensor_dtype: Data type for tensors (e.g., torch.bfloat16).
group: PyTorch distributed process group.
"""
_next_id: ClassVar[int] = 0
_runtime_registry: ClassVar[dict[int, "All2AllCpp"]] = {}
@staticmethod
def _release_comm(comm_id: int, runtime: "All2AllCpp") -> None:
All2All._runtime_registry.pop(comm_id, None)
runtime.destroy()
def __init__(
self,
rank: int,
world_size: int,
seqlen: int,
hidden_dim: int,
num_sms: int,
tensor_dtype: torch.dtype,
group: torch.distributed.ProcessGroup | None = None,
timeout_seconds: float | None = None,
) -> None:
self.rank = rank
self.world_size = world_size
self.num_sms = num_sms
self.tensor_dtype = tensor_dtype
self.buffer_size = int(seqlen * hidden_dim * tensor_dtype.itemsize)
# Initialize the C++ runtime. Omit timeout_seconds to keep the kernel's default
# (DEFAULT_BARRIER_TIMEOUT_SECONDS); it can still be changed later via set_timeout_seconds.
if timeout_seconds is None:
self.runtime = All2AllCpp(rank, world_size, seqlen, hidden_dim, num_sms, tensor_dtype)
else:
if not math.isfinite(timeout_seconds) or timeout_seconds < 0:
raise ValueError(f"all2all timeout seconds must be finite and non-negative, got {timeout_seconds}")
self.runtime = All2AllCpp(rank, world_size, seqlen, hidden_dim, num_sms, tensor_dtype, timeout_seconds)
# Register in the class-level table so the custom ops can find us.
# Auto-cleanup via weakref.finalize covers the case where destroy()
# isn't called explicitly — without it the registry would keep the
# runtime (and CUDA/IPC resources) alive for the process lifetime.
self._comm_id = All2All._next_id
All2All._next_id += 1
All2All._runtime_registry[self._comm_id] = self.runtime
self._finalizer = weakref.finalize(self, All2All._release_comm, self._comm_id, self.runtime)
# Exchange IPC handles across all ranks
ipc_handles: list[Any] = [None] * world_size
local_ipc_handle = self.runtime.get_local_ipc_handle()
dist.all_gather_object(ipc_handles, local_ipc_handle, group)
self.runtime.sync(ipc_handles)
def set_rank_tokens(self, rank_num_tokens: list[int]) -> None:
"""Sets per-rank token counts on the C++ runtime."""
self.runtime.set_rank_tokens(rank_num_tokens)
def set_timeout_seconds(self, seconds: float) -> None:
"""Set the all2all barrier (deadlock-detection) timeout, in seconds.
The C++ runtime converts to clock cycles using the device's peak SM clock. Raise it
during the first ``torch.compile`` forward, where one rank's recompile can delay its
all2all launch past the steady-state timeout; reset for steady state.
"""
if not math.isfinite(seconds) or seconds < 0:
raise ValueError(f"all2all timeout seconds must be finite and non-negative, got {seconds}")
self.runtime.set_timeout_seconds(seconds)
def send_recv_heads(self, x: torch.Tensor, *, copy_out: bool = False) -> torch.Tensor:
"""Exchange attention heads across ranks (All2All pattern).
Args:
x: Input tensor of shape [batch, tokens, heads, head_dim].
copy_out: If True, copy result to a new tensor instead of using buffer.
Returns:
Output tensor with redistributed heads.
"""
return torch.ops.ltx_kernels.send_recv_heads(x, self._comm_id, self.world_size, copy_out)
def gather_heads(self, x: torch.Tensor, *, copy_out: bool = False) -> torch.Tensor:
"""Gather heads back to original distribution (reverse All2All).
Args:
x: Input tensor with distributed heads.
copy_out: If True, copy result to a new tensor instead of using buffer.
Returns:
Output tensor with gathered heads.
"""
return torch.ops.ltx_kernels.gather_heads(x, self._comm_id, self.world_size, copy_out)
def allgather(self, x: torch.Tensor, *, copy_out: bool = False) -> torch.Tensor:
"""Allgather operation across all ranks.
Args:
x: Input tensor to gather.
copy_out: If True, copy result to a new tensor instead of using buffer.
Returns:
Gathered tensor from all ranks.
"""
return self.runtime.allgather(x, copy_out)
def destroy(self) -> None:
"""Release IPC handles and GPU memory buffers."""
self._finalizer()
@@ -0,0 +1,21 @@
"""GPU architecture detection for blockwise kernel dispatch."""
import torch
def get_device_arch() -> str:
"""Return a coarse architecture name for the current CUDA device.
Used to pick the FP8 GEMM kernel variant (SM89 vs SM90) at runtime.
"""
if not torch.cuda.is_available():
raise RuntimeError("CUDA is not available; blockwise kernels require a CUDA device.")
major, minor = torch.cuda.get_device_capability(torch.cuda.current_device())
if major == 8 and (minor >= 0 and minor < 9):
return "ampere"
if major == 8 and minor == 9:
return "ada"
if major == 9 and minor == 0:
return "hopper"
if major in {10, 12}:
return "blackwell"
raise NotImplementedError(f"Unsupported GPU compute capability sm_{major}{minor}.")
@@ -0,0 +1,36 @@
"""Blockwise FP8/FP6 quantization kernels.
Importing this subpackage pulls in the compiled ``ops_cpp`` / ``blockwise_cpp``
extensions (via :mod:`.functional` and :mod:`.linear`) and ``triton``, so it
raises :class:`ImportError` on hosts where the kernels were not built. Callers
that want a soft dependency should guard the import (e.g. ``pytest.importorskip``
or the lazy gate in ``ltx_core.quantization.blockwise``).
"""
from ltx_kernels.blockwise.functional import (
blockwise_dequantize,
blockwise_quantize_adanorm_triton,
blockwise_quantize_rms_fma_triton,
fp6_blockwise_quantize_weights_torch,
fp6_pack_tensor,
fp6_unpack_tensor,
fp8_blockwise_quantize_weights_torch,
gated_attention_triton,
rms_norm_rope,
rms_norm_split_rope,
)
from ltx_kernels.blockwise.linear import BlockwiseFP6Linear, BlockwiseFP8Linear
__all__ = [
"BlockwiseFP6Linear",
"BlockwiseFP8Linear",
"blockwise_dequantize",
"blockwise_quantize_adanorm_triton",
"blockwise_quantize_rms_fma_triton",
"fp6_blockwise_quantize_weights_torch",
"fp6_pack_tensor",
"fp6_unpack_tensor",
"fp8_blockwise_quantize_weights_torch",
"gated_attention_triton",
"rms_norm_rope",
"rms_norm_split_rope",
]
@@ -0,0 +1,234 @@
"""Blockwise FP8/FP6 functional ops.
The CUDA kernels live in the ``ops_cpp`` extension.
"""
from typing import Optional, Tuple
import torch
from ops_cpp import fp6_pack, fp6_unpack
from ops_cpp import rms_norm_rope as rms_norm_rope_cuda
from ops_cpp import rms_norm_split_rope as rms_norm_split_rope_cuda
from ltx_kernels.blockwise.triton_ops import (
blockwise_dequantize_triton,
blockwise_quantize_adanorm_triton, # noqa: F401 (re-exported for consumers)
blockwise_quantize_rms_fma_triton, # noqa: F401 (re-exported for consumers)
blockwise_quantize_triton, # noqa: F401 (used by linear.BlockwiseGemmLinearFunc)
gated_attention_triton, # noqa: F401 (re-exported for consumers)
)
# Quantization scale constants for different precisions
FP8_SCALE_MAX = 448.0
FP6_SCALE_MAX = 0.1172
@torch.library.custom_op("q8_kernels_ops::rms_norm_rope", mutates_args=())
def _rms_norm_rope_cuda(
x: torch.Tensor,
weights: Optional[torch.Tensor],
cos_freqs: torch.Tensor,
sin_freqs: torch.Tensor,
out_16bit: bool,
) -> torch.Tensor:
return rms_norm_rope_cuda(x, weights, cos_freqs, sin_freqs, out_16bit)
@torch.library.register_fake("q8_kernels_ops::rms_norm_rope")
def _rms_norm_rope_cuda_fake(
x: torch.Tensor,
weights: Optional[torch.Tensor],
cos_freqs: torch.Tensor,
sin_freqs: torch.Tensor,
out_16bit: bool,
) -> torch.Tensor:
out = torch.empty_like(x)
if out_16bit:
return out.to(torch.bfloat16)
else:
return out.to(torch.float8_e4m3fn)
@torch.library.custom_op("q8_kernels_ops::rms_norm_split_rope", mutates_args=())
def _rms_norm_split_rope_cuda(
x: torch.Tensor,
sin_freqs: torch.Tensor,
cos_freqs: torch.Tensor,
weights: torch.Tensor,
out_fp8: bool,
) -> torch.Tensor:
return rms_norm_split_rope_cuda(x, sin_freqs, cos_freqs, weights, out_fp8)
@torch.library.register_fake("q8_kernels_ops::rms_norm_split_rope")
def _rms_norm_split_rope_cuda_fake(
x: torch.Tensor,
sin_freqs: torch.Tensor,
cos_freqs: torch.Tensor,
weights: torch.Tensor,
out_fp8: bool,
) -> torch.Tensor:
out = torch.empty_like(x)
if out_fp8:
return out.to(torch.float8_e4m3fn)
else:
return out.to(torch.bfloat16)
class RMSNormRope(torch.autograd.Function):
@staticmethod
def forward(ctx, x, weights, cos_freqs, sin_freqs, out_16bit):
return torch.ops.q8_kernels_ops.rms_norm_rope(
x, weights, cos_freqs, sin_freqs, out_16bit
)
class RMSNormSplitRope(torch.autograd.Function):
@staticmethod
def forward(ctx, x, sin_freqs, cos_freqs, weights, out_fp8):
return torch.ops.q8_kernels_ops.rms_norm_split_rope(
x, sin_freqs, cos_freqs, weights, out_fp8
)
def rms_norm_rope(
x: torch.Tensor,
cos_freqs: torch.Tensor,
sin_freqs: torch.Tensor,
weights: Optional[torch.Tensor] = None,
out_fp8: bool = False,
) -> torch.Tensor:
# weights=None flows through to the kernel's no-affine path (nullptr); see
# rms_norm_rope.cpp, which dispatches the norm_affine template on has_value().
return RMSNormRope.apply(x, weights, cos_freqs, sin_freqs, not out_fp8)
def rms_norm_split_rope(
x: torch.Tensor,
cos_freqs: torch.Tensor,
sin_freqs: torch.Tensor,
weights: Optional[torch.Tensor] = None,
out_fp8: bool = False,
) -> torch.Tensor:
"""
Apply RMS normalization followed by split RoPE (Rotary Position Embedding).
Args:
x: Input tensor of shape [b, s, h] where h can be 2048, 4096, or 8192
cos_freqs: Cos frequencies of shape [b, n, s, d] where n=32 and n*d = h/2
sin_freqs: Sin frequencies of shape [b, n, s, d] where n=32
weights: Optional RMS norm weights of shape [h]
out_fp8: If True, output in float8_e4m3fn, otherwise bfloat16
Returns:
Output tensor of shape [b, s, h] with dtype based on out_fp8 parameter
"""
if weights is None:
weights = torch.ones(x.shape[-1], dtype=x.dtype, device=x.device)
return RMSNormSplitRope.apply(x, sin_freqs, cos_freqs, weights, out_fp8)
def blockwise_quantize_weights(w: torch.Tensor, block_size=128, scale_max=FP8_SCALE_MAX) -> tuple[torch.Tensor, torch.Tensor]:
w = w.view(w.shape[0]//block_size, block_size, w.shape[1]//block_size, block_size).transpose(1, 2).contiguous()
w_absmax = w.float().abs().view(w.shape[0], w.shape[1], block_size*block_size).max(dim=-1, keepdim=False).values
w_scales = scale_max/w_absmax
w_quant = (w.float()*w_scales[:, :, None, None].float()).to(torch.float8_e4m3fn)
w_quant = w_quant.transpose(1, 2).contiguous()
w_quant = w_quant.view(w.shape[0]*block_size, -1)
return w_quant, (1/w_scales).contiguous()
def blockwise_quantize_torch(x: torch.Tensor, block_size=128, scale_max=FP8_SCALE_MAX) -> tuple[torch.Tensor, torch.Tensor]:
b, n, h = x.shape
x = x.view(-1, x.shape[-1]//block_size, block_size)
x_absmax = x.float().abs().max(dim=-1, keepdim=True).values
x_scales = scale_max/x_absmax
x_scaled = (x * x_scales).to(torch.float8_e4m3fn)
return x_scaled.view(b, n, h), 1/x_scales.view(b*n, h//block_size).t().contiguous().float()
# Precision-specific convenience functions
def fp8_blockwise_quantize_weights_torch(x: torch.Tensor, block_size=128) -> tuple[torch.Tensor, torch.Tensor]:
return blockwise_quantize_weights(x, block_size, FP8_SCALE_MAX)
def fp6_blockwise_quantize_weights_torch(x: torch.Tensor, block_size=128) -> tuple[torch.Tensor, torch.Tensor]:
return blockwise_quantize_weights(x, block_size, FP6_SCALE_MAX)
def blockwise_dequantize(x: Tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor:
x_fp8, scales = x
return blockwise_dequantize_triton(x_fp8, scales)
# FP6 Pack/Unpack operations
@torch.library.custom_op("q8_kernels_ops::fp6_pack", mutates_args=())
def _fp6_pack(x: torch.Tensor) -> torch.Tensor:
"""Pack 8-bit tensor to 6-bit by dropping e_1 and e_2 bits.
Args:
x: Input tensor of shape [m, n] with dtype uint8 or float8_e4m3fn
Returns:
Packed tensor of shape [m, n*3/4] with dtype uint8
"""
return fp6_pack(x)
@torch.library.register_fake("q8_kernels_ops::fp6_pack")
def _fp6_pack_fake(x: torch.Tensor) -> torch.Tensor:
m, n = x.shape
n_packed = (n * 3) // 4
return torch.empty(m, n_packed, dtype=torch.uint8, device=x.device)
@torch.library.custom_op("q8_kernels_ops::fp6_unpack", mutates_args=())
def _fp6_unpack(x: torch.Tensor, original_n: int) -> torch.Tensor:
"""Unpack 6-bit tensor back to 8-bit (with e_1 and e_2 set to 0).
Args:
x: Packed tensor of shape [m, n_packed] with dtype uint8
original_n: Original dimension size (unpacked)
Returns:
Unpacked tensor of shape [m, original_n] with dtype uint8
"""
return fp6_unpack(x, original_n)
@torch.library.register_fake("q8_kernels_ops::fp6_unpack")
def _fp6_unpack_fake(x: torch.Tensor, original_n: int) -> torch.Tensor:
m = x.shape[0]
return torch.empty(m, original_n, dtype=torch.uint8, device=x.device)
# Convenience wrapper functions
def fp6_pack_tensor(x: torch.Tensor) -> torch.Tensor:
"""Pack 8-bit tensor to 6-bit by dropping e_1 and e_2 bits.
This function packs FP8 weights into a more memory-efficient FP6 format
by dropping the two highest exponent bits (e_1 and e_2). This achieves
25% memory reduction (4 bytes -> 3 bytes per 4 elements).
Args:
x: Input tensor of shape [m, n] with dtype uint8 or float8_e4m3fn.
n must be divisible by 8.
Returns:
Packed tensor of shape [m, n*3/4] with dtype uint8
Example:
>>> w_fp8 = torch.randn(1024, 4096, dtype=torch.float8_e4m3fn, device='cuda')
>>> w_fp8_uint8 = w_fp8.view(torch.uint8)
>>> w_packed = fp6_pack_tensor(w_fp8_uint8)
>>> print(w_packed.shape) # [1024, 3072]
"""
return torch.ops.q8_kernels_ops.fp6_pack(x)
def fp6_unpack_tensor(x: torch.Tensor, original_n: int) -> torch.Tensor:
"""Unpack 6-bit tensor back to 8-bit (with e_1 and e_2 set to 0).
This function unpacks FP6 weights back to FP8 format. Note that the
two dropped exponent bits (e_1 and e_2) are restored as 0.
Args:
x: Packed tensor of shape [m, n_packed] with dtype uint8
original_n: Original dimension size before packing (must be divisible by 8)
Returns:
Unpacked tensor of shape [m, original_n] with dtype uint8
Example:
>>> w_packed = torch.randint(0, 256, (1024, 3072), dtype=torch.uint8, device='cuda')
>>> w_unpacked = fp6_unpack_tensor(w_packed, 4096)
>>> w_fp8 = w_unpacked.view(torch.float8_e4m3fn)
>>> print(w_fp8.shape) # [1024, 4096]
"""
return torch.ops.q8_kernels_ops.fp6_unpack(x, original_n)
@@ -0,0 +1,279 @@
"""Blockwise FP8/FP6 quantized linear layers and the FP8 GEMM custom op.
The FP8 GEMM kernels live in the ``blockwise_cpp`` extension.
"""
from typing import List, Optional
import torch
import torch.nn as nn
from ops_cpp import fp6_pack, fp6_unpack
from ltx_kernels.arch import get_device_arch
from ltx_kernels.blockwise.functional import (
blockwise_quantize_triton,
blockwise_quantize_weights,
fp6_blockwise_quantize_weights_torch,
)
# Lazily-initialized, architecture-specific FP8 GEMM callable. Importing this
# module must not require a CUDA device or the compiled extension; the kernel is
# only resolved on first use (matching the lazy-import gate in ltx-core's
# blockwise.__init__).
_fp8_gemm = None
def get_fp8_gemm_nt():
"""Runtime GPU architecture dispatch for FP8 GEMM."""
arch = get_device_arch()
if arch in ["ada", "blackwell"]:
# Ada/Blackwell use SM89 kernels (GeForce path)
from blockwise_cpp import fp8_gemm_nt_sm89
def _func(a, b, d, bias=None, c=None, num_sms=132, use_fast_accum=True):
return fp8_gemm_nt_sm89(a, b, d, bias=bias, use_fast_accum=use_fast_accum)
return _func
elif arch == "hopper":
# Hopper uses SM90 kernels (H100 path)
from blockwise_cpp import fp8_gemm_nt_sm90
def _func(a, b, d, bias=None, c=None, num_sms=132, use_fast_accum=True):
return fp8_gemm_nt_sm90(a, b, d, bias=bias, c=c, num_sms=num_sms)
return _func
else:
raise RuntimeError(f"Unsupported GPU architecture: {arch}")
def _fp8_gemm_dispatch(a, b, d, bias=None, c=None, num_sms=132, use_fast_accum=True):
global _fp8_gemm
if _fp8_gemm is None:
_fp8_gemm = get_fp8_gemm_nt()
return _fp8_gemm(a, b, d, bias=bias, c=c, num_sms=num_sms, use_fast_accum=use_fast_accum)
@torch.library.custom_op("blockwise::fp8_gemm", mutates_args=())
def blockwise_fp8_gemm(
a: List[torch.Tensor],
b: List[torch.Tensor],
bias: Optional[torch.Tensor],
use_fast_accum: bool,
) -> torch.Tensor:
d = torch.empty(a[0].shape[0], b[0].shape[0], dtype=torch.bfloat16, device=b[0].device)
_fp8_gemm_dispatch(a, b, d, bias=bias, use_fast_accum=use_fast_accum)
return d
@blockwise_fp8_gemm.register_fake
def blockwise_fp8_gemm_fake(
a: List[torch.Tensor],
b: List[torch.Tensor],
bias: Optional[torch.Tensor],
use_fast_accum: bool,
) -> torch.Tensor:
o = torch.empty(a[0].shape[0], b[0].shape[0], dtype=torch.bfloat16, device=b[0].device)
return o
def is_16bit(x) -> bool:
return x.dtype == torch.float16 or x.dtype == torch.bfloat16
class BlockwiseGemmLinearFunc(torch.autograd.Function):
@staticmethod
def forward(
ctx,
a: tuple[torch.Tensor, torch.Tensor],
w: tuple[torch.Tensor],
bias: Optional[torch.Tensor],
) -> torch.Tensor:
is_16bit_a = is_16bit(a[0])
if is_16bit_a:
a = blockwise_quantize_triton(a[0])
b, n, h = a[0].shape
out_h, _ = w[0].shape
new_a = (a[0].view(-1, h), a[1])
d = blockwise_fp8_gemm(new_a, w, bias, use_fast_accum=True)
return d.view(b, n, out_h)
def blockwise_linear_func(a, b, bias=None):
return BlockwiseGemmLinearFunc.apply(a, b, bias)
class BlockwiseLinear(nn.Module):
"""Base class for blockwise quantized linear layers."""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
device=None,
dtype=None, # for compatibility with nn.Linear
):
super().__init__()
self.in_features = in_features
self.out_features = out_features
if bias:
# FP32 to match the SM89/SM90 GEMM bias contract (the kernels take float*).
self.bias = nn.Parameter(
torch.empty(out_features, device=device, dtype=torch.float32),
requires_grad=False,
)
else:
self.register_parameter("bias", None)
@property
def fp8weight(self):
"""Should be implemented by subclasses."""
raise NotImplementedError("Subclasses must implement the weight property")
def forward(self, x):
if not isinstance(x, tuple):
x = (x, None)
return blockwise_linear_func(x, (self.fp8weight, self.weight_scale), self.bias)
@classmethod
def from_linear(cls, linear: nn.Linear, transform_weights=True):
"""Should be implemented by subclasses."""
raise NotImplementedError("Subclasses must implement from_linear method")
class BlockwiseFP8Linear(BlockwiseLinear):
"""Blockwise quantized linear layer using FP8 weights."""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
device=None,
dtype=None,
):
super().__init__(in_features, out_features, bias, device, dtype)
assert in_features % 128 == 0, f"in_features must be divisible by 128, got {in_features}"
assert out_features % 128 == 0, f"out_features must be divisible by 128, got {out_features}"
self.weight = nn.Parameter(
torch.empty(
out_features, in_features, device=device, dtype=torch.float8_e4m3fn
),
requires_grad=False,
)
self.weight_scale = nn.Parameter(
torch.ones(out_features//128, in_features//128, device=device, dtype=torch.float32),
requires_grad=False,
)
@property
def fp8weight(self):
"""Return FP8 weight directly."""
return self.weight
@classmethod
def from_linear(cls, linear: nn.Linear, transform_weights=True):
layer = cls(
linear.in_features,
linear.out_features,
linear.bias is not None,
device=linear.weight.device,
)
if transform_weights:
w_fp8, w_scales = blockwise_quantize_weights(linear.weight.data.cuda())
else:
w_fp8 = torch.empty(linear.out_features, linear.in_features, dtype=torch.float8_e4m3fn, device=linear.weight.device)
w_scales = torch.ones(
w_fp8.shape[0]//128,
w_fp8.shape[1]//128,
device=w_fp8.device,
dtype=torch.float32,
)
layer.weight.data = w_fp8
layer.weight_scale.data = w_scales
if linear.bias is not None:
layer.bias.data = linear.bias.data.to(torch.float32)
return layer
class BlockwiseFP6Linear(BlockwiseLinear):
"""Blockwise quantized linear layer using FP6 weights with 6-bit packing."""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
device=None,
dtype=None,
):
assert in_features % 4 == 0, f"in_features must be divisible by 4, got {in_features}"
assert in_features % 8 == 0, f"in_features must be divisible by 8 for fp6_pack, got {in_features}"
assert in_features % 128 == 0, f"in_features must be divisible by 128, got {in_features}"
assert out_features % 128 == 0, f"out_features must be divisible by 128, got {out_features}"
super().__init__(in_features, out_features, bias, device, dtype)
# Store packed weight: [out_features, (in_features // 4)*3]
self.weight = nn.Parameter(
torch.empty(
out_features, (in_features // 4)*3, device=device, dtype=torch.uint8
),
requires_grad=False,
)
# Scales are based on unpacked dimensions
self.weight_scale = nn.Parameter(
torch.ones(out_features//128, in_features//128, device=device, dtype=torch.float32),
requires_grad=False,
)
@property
def fp8weight(self):
"""Unpack the 6-bit weights to 8-bit format on-the-fly."""
# Unpack from [out_features, (in_features // 4)*3] to [out_features, in_features]
unpacked = fp6_unpack(self.weight, self.in_features)
# Convert uint8 back to float8_e4m3fn view
return unpacked.view(torch.float8_e4m3fn)
@classmethod
def from_linear(cls, linear: nn.Linear, transform_weights=True):
layer = cls(
linear.in_features,
linear.out_features,
linear.bias is not None,
device=linear.weight.device,
)
if transform_weights:
# Quantize to FP6 (actually FP8 with restricted range)
w_fp6, w_scales = fp6_blockwise_quantize_weights_torch(linear.weight.data.cuda())
# Pack the FP6 weights from [out, in] uint8 to [out, in*3/4] uint8
w_fp6_uint8 = w_fp6.view(torch.uint8)
w_packed = fp6_pack(w_fp6_uint8)
layer.weight.data = w_packed
layer.weight_scale.data = w_scales
else:
# If not transforming, assume weights are already in packed format
layer.weight.data = torch.empty(linear.out_features, (linear.in_features // 4) * 3, dtype=torch.uint8, device=linear.weight.device)
layer.weight_scale.data = torch.ones(
linear.weight.shape[0]//128,
linear.in_features//128,
device=linear.weight.device,
dtype=torch.float32,
)
if linear.bias is not None:
layer.bias.data = linear.bias.data.to(torch.float32)
return layer
@@ -0,0 +1,453 @@
from typing import *
import torch
from triton import jit
from triton import language as tl
from triton import next_power_of_2
from torch.library import triton_op, wrap_triton
def get_tma_aligned_size(n, element_size):
num_elems_tma = 16 // element_size
return ((n + num_elems_tma - 1) // num_elems_tma) * num_elems_tma
@jit
def _quantize(x, scale_max: tl.constexpr, NUM_BLOCKS: tl.constexpr, BLOCK_SIZE: tl.constexpr):
x = tl.reshape(x, (NUM_BLOCKS, BLOCK_SIZE))
x_abs = tl.abs(x)
x_abs = tl.broadcast_to(x_abs, (NUM_BLOCKS, BLOCK_SIZE))
x_absmax = tl.max(x_abs, axis=1)[:, None]
x_scales = scale_max / x_absmax
x_quant = (x_scales * x).to(tl.float8e4nv)
x_out_scales = 1.0/x_scales
return x_quant, x_out_scales
@jit
def _kernel(X, OUT, SCALES, HDIM, BLOCK_SIZE: tl.constexpr):
row_idx = tl.program_id(0)
x_ptr = X + row_idx * HDIM
out_ptr = OUT + row_idx * HDIM
h_offset = tl.arange(0, BLOCK_SIZE)
x = tl.load(x_ptr + h_offset, mask=h_offset < HDIM).to(tl.float32)
x_scale = 127.0 / tl.max(tl.abs(x))
x_scaled = x * x_scale
x_scaled += (0.5 * tl.where(x_scaled >= 0, 1, -1)).to(tl.int8)
tl.store(out_ptr + h_offset, x_scaled, mask=h_offset < HDIM)
tl.store(SCALES + row_idx, 1 / x_scale)
def run_quantize_kernel(x: torch.Tensor, out_dtype: Optional[torch.dtype] = None):
x_shape_orig = x.shape
x = x.view(-1, x_shape_orig[-1])
out = torch.empty(x_shape_orig, dtype=torch.int8, device=x.device)
scales = torch.empty(x.shape[0], dtype=torch.float, device=x.device)
BLOCK_SIZE = next_power_of_2(x_shape_orig[-1])
grid = (x.shape[0],)
_kernel[grid](x, out, scales, x_shape_orig[-1], BLOCK_SIZE, num_warps=4)
return out.view(x_shape_orig), scales.view(x_shape_orig[:-1])
@jit
def _block_quant_norm_kernel(
X,
Norm_Scale,
Norm_Shift,
Out_scales,
X_out,
norm_scale_batch_stride: int,
norm_shift_batch_stride: int,
norm_scale_token_stride: int,
norm_shift_token_stride: int,
seqlen: int,
H: tl.constexpr,
BROADCAST_SEQLEN: tl.constexpr,
NUM_BLOCKS: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
TMA_ALIGNED_MN: tl.constexpr,
SCALE_MAX: tl.constexpr,
):
token_idx = tl.program_id(0)
batch_id = token_idx // seqlen
x_ptr = X + token_idx*H + tl.arange(0, H)
if BROADCAST_SEQLEN:
norm_scale_ptr = Norm_Scale + batch_id*norm_scale_batch_stride + tl.arange(0, H)
norm_shift_ptr = Norm_Shift + batch_id*norm_shift_batch_stride + tl.arange(0, H)
else:
norm_scale_ptr = Norm_Scale + batch_id*norm_scale_batch_stride + (token_idx % seqlen) * norm_scale_token_stride + tl.arange(0, H)
norm_shift_ptr = Norm_Shift + batch_id*norm_shift_batch_stride + (token_idx % seqlen) * norm_shift_token_stride + tl.arange(0, H)
norm_scales = tl.load(norm_scale_ptr)
norm_shift = tl.load(norm_shift_ptr)
x = tl.load(x_ptr)
x_sqr = x*x
x_norm = tl.sum(x_sqr) / H
x_norm = tl.rsqrt(x_norm + 0.00001)
x = (x * x_norm).to(tl.bfloat16)
x = x * (1.0+norm_scales) + norm_shift
o_quant, o_scales = _quantize(x, SCALE_MAX, NUM_BLOCKS, BLOCK_SIZE)
x_out_ptr = X_out + token_idx*H + BLOCK_SIZE*tl.arange(0, NUM_BLOCKS)[:, None] + tl.arange(0, BLOCK_SIZE)[None, :]
tl.store(x_out_ptr, o_quant)
out_scales_ptr = Out_scales + token_idx + TMA_ALIGNED_MN*tl.arange(0, NUM_BLOCKS)[:, None]
tl.store(out_scales_ptr, o_scales)
@jit
def _gelu(x):
return x * tl.sigmoid(1.702*x)
@jit
def _block_quant_kernel(
X,
Out_scales,
X_out,
seqlen: int,
H: tl.constexpr,
NUM_BLOCKS: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
TMA_ALIGNED_MN: tl.constexpr,
USE_GELU: tl.constexpr,
SCALE_MAX: tl.constexpr,
):
batch_id, seq_id = tl.program_id(0), tl.program_id(1)
token_idx = batch_id*seqlen + seq_id
x_ptr = X + token_idx*H + BLOCK_SIZE*tl.arange(0, NUM_BLOCKS)[:, None] + tl.arange(0, BLOCK_SIZE)[None, :]
x = tl.load(x_ptr).to(tl.float32)
if USE_GELU:
x = _gelu(x)
o_quant, o_scales = _quantize(x, SCALE_MAX, NUM_BLOCKS, BLOCK_SIZE)
x_out_ptr = X_out + token_idx*H + BLOCK_SIZE*tl.arange(0, NUM_BLOCKS)[:, None] + tl.arange(0, BLOCK_SIZE)[None, :]
tl.store(x_out_ptr, o_quant)
out_scales_ptr = Out_scales + token_idx + TMA_ALIGNED_MN*tl.arange(0, NUM_BLOCKS)[:, None]
tl.store(out_scales_ptr, o_scales)
@triton_op("blockwise::quantize", mutates_args=())
def _quant_blockwise_tma_aligned_func(x: torch.Tensor, use_gelu: bool) -> Tuple[torch.Tensor, torch.Tensor]:
b, s, h = x.shape
num_rows = b*s
block_size = 128
num_blocks = h // block_size
out = torch.empty((num_rows, h), device=x.device, dtype=torch.float8_e4m3fn)
tma_aligned_mn = get_tma_aligned_size(num_rows, 4)
scales = torch.empty_strided((num_rows, num_blocks), (1, tma_aligned_mn), dtype=torch.float, device=x.device)
scale_max: float = 448.0
wrap_triton(_block_quant_kernel)[(b, s)](
x,
scales,
out,
H=h,
seqlen=s,
BLOCK_SIZE=128,
NUM_BLOCKS=num_blocks,
TMA_ALIGNED_MN=tma_aligned_mn,
USE_GELU=use_gelu,
SCALE_MAX=scale_max
)
return out.view(b, s, h), scales
def run_quant_blockwise_tma_aligned(x):
return _quant_blockwise_tma_aligned_func(x, False)
def run_quant_blockwise_gelu_tma_aligned(x):
return _quant_blockwise_tma_aligned_func(x, True)
@triton_op("blockwise::adanorm", mutates_args=())
def run_quant_blockwise_norm_tma_aligned(x: torch.Tensor, w: Optional[torch.Tensor], norm_scale: torch.Tensor, norm_shift: torch.Tensor, out_dtype: torch.dtype, hd_scale: Optional[float]) -> Tuple[torch.Tensor, torch.Tensor]:
b, s, h = x.shape
is_broadcast = (norm_scale.shape[1] == 1)
num_rows = b*s
block_size = 128
num_blocks = h // block_size
out = torch.empty((num_rows, h), device=x.device, dtype=torch.float8_e4m3fn)
tma_aligned_mn = get_tma_aligned_size(num_rows, 4)
scales = torch.empty_strided((num_rows, num_blocks), (1, tma_aligned_mn), dtype=torch.float, device=x.device)
norm_scale_batch_stride = norm_scale.stride(0)
norm_shift_batch_stride = norm_shift.stride(0)
scale_max: float = 448.0
wrap_triton(_block_quant_norm_kernel)[(num_rows, 1, 1)](
x,
norm_scale,
norm_shift,
scales,
out,
norm_scale_batch_stride=norm_scale_batch_stride,
norm_shift_batch_stride=norm_shift_batch_stride,
norm_scale_token_stride=norm_scale.stride(1),
norm_shift_token_stride=norm_shift.stride(1),
H=h,
seqlen=s,
BROADCAST_SEQLEN=is_broadcast,
BLOCK_SIZE=128,
NUM_BLOCKS=num_blocks,
TMA_ALIGNED_MN=tma_aligned_mn,
SCALE_MAX=scale_max
)
return out.view(b, s, h), scales
@jit
def _quant_rms_sum_mult_kernel(
X,
Y,
Z,
Out,
Out_scales,
seqlen: int,
z_batch_stride: int,
z_token_stride: int,
TMA_ALIGNED_MN: int,
H: tl.constexpr,
NUM_BLOCKS: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
IS_Z_BROADCAST: tl.constexpr,
QUANTIZE: tl.constexpr,
SCALE_MAX: tl.constexpr,
):
token_idx = tl.program_id(0)
batch_id = token_idx // seqlen
x_ptr = X + token_idx*H + tl.arange(0, H)
y_ptr = Y + token_idx*H + tl.arange(0, H)
if IS_Z_BROADCAST:
z_ptr = Z + batch_id*z_batch_stride + tl.arange(0, H)
else:
z_ptr = Z + batch_id*z_batch_stride + (token_idx % seqlen) * z_token_stride + tl.arange(0, H)
x = tl.load(x_ptr)
y = tl.load(y_ptr)
z = tl.load(z_ptr)
o = x + y*z
tl.store(x_ptr, o)
o_sqr = o * o
o_norm = tl.sum(o_sqr, axis=0) / H
o_inv = tl.rsqrt(o_norm)
o *= o_inv
if QUANTIZE:
o_quant, o_scales = _quantize(o, SCALE_MAX, NUM_BLOCKS, BLOCK_SIZE)
x_out_ptr = Out + token_idx*H + BLOCK_SIZE*tl.arange(0, NUM_BLOCKS)[:, None] + tl.arange(0, BLOCK_SIZE)[None, :]
tl.store(x_out_ptr, o_quant)
out_scales_ptr = Out_scales + token_idx + TMA_ALIGNED_MN*tl.arange(0, NUM_BLOCKS)[:, None]
tl.store(out_scales_ptr, o_scales)
else:
x_out_ptr = Out + token_idx*H + tl.arange(0, H)
tl.store(x_out_ptr, o)
@triton_op("blockwise::quant_rms_fma", mutates_args=("x", ))
def run_quant_blockwise_rms_fma(x: torch.Tensor, y: torch.Tensor, z: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
b, s, h = x.shape
scale_max: float = 448.0
is_z_broadcast = (z.shape[1] == 1)
num_rows = b*s
block_size = 128
num_blocks = h // block_size
out = torch.empty((num_rows, h), device=x.device, dtype=torch.float8_e4m3fn)
tma_aligned_mn = get_tma_aligned_size(num_rows, 4)
scales = torch.empty_strided((num_rows, num_blocks), (1, tma_aligned_mn), dtype=torch.float, device=x.device)
z_batch_stride = z.stride(0)
wrap_triton(_quant_rms_sum_mult_kernel)[(num_rows, 1, 1)](
x,
y,
z,
out,
scales,
seqlen=s,
z_batch_stride=z_batch_stride,
z_token_stride=z.stride(1),
TMA_ALIGNED_MN=tma_aligned_mn,
H=h,
NUM_BLOCKS=num_blocks,
BLOCK_SIZE=128,
IS_Z_BROADCAST=is_z_broadcast,
QUANTIZE=True,
SCALE_MAX=scale_max
)
return out.view(b, s, h), scales
@triton_op("blockwise::rms_fma", mutates_args=("x", ))
def run_rms_fma(x: torch.Tensor, y: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
b, s, h = x.shape
is_z_broadcast = (z.shape[1] == 1)
num_rows = b*s
block_size = 128
num_blocks = h // block_size
out = torch.empty((num_rows, h), device=x.device, dtype=torch.bfloat16)
z_batch_stride = z.stride(0)
wrap_triton(_quant_rms_sum_mult_kernel)[(num_rows, 1, 1)](
x,
y,
z,
out,
None,
seqlen=s,
z_batch_stride=z_batch_stride,
z_token_stride=z.stride(1),
TMA_ALIGNED_MN=0,
H=h,
NUM_BLOCKS=num_blocks,
BLOCK_SIZE=128,
IS_Z_BROADCAST=is_z_broadcast,
QUANTIZE=False,
SCALE_MAX=448.0
)
return out.view(b, s, h)
@jit
def _gated_attention_kernel(
X,
Gate_Logits,
Out,
Out_scales,
H: tl.constexpr,
NUM_HEADS: tl.constexpr,
DIM_HEAD: tl.constexpr,
NUM_BLOCKS: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
TMA_ALIGNED_MN: tl.constexpr,
QUANTIZE: tl.constexpr,
SCALE_MAX: tl.constexpr,
):
token_idx = tl.program_id(0)
# Load gate logits for all heads: shape [NUM_HEADS]
gate_logits = tl.load(Gate_Logits + token_idx * NUM_HEADS + tl.arange(0, NUM_HEADS)).to(tl.float32)
# 2*sigmoid so that zero-init gives identity (2 * 0.5 = 1.0)
gates = 2.0 * tl.sigmoid(gate_logits) # [NUM_HEADS]
gates = tl.broadcast_to(gates[:, None], (NUM_HEADS, DIM_HEAD)) # [NUM_HEADS, DIM_HEAD]
# Load x viewed as [NUM_HEADS, DIM_HEAD]
offsets = tl.arange(0, NUM_HEADS)[:, None] * DIM_HEAD + tl.arange(0, DIM_HEAD)[None, :]
x = tl.load(X + token_idx * H + offsets).to(tl.float32)
# Apply per-head gating
gated = x * gates # [NUM_HEADS, DIM_HEAD]
if QUANTIZE:
o_quant, o_scales = _quantize(gated, SCALE_MAX, NUM_BLOCKS, BLOCK_SIZE)
out_ptr = Out + token_idx * H + BLOCK_SIZE * tl.arange(0, NUM_BLOCKS)[:, None] + tl.arange(0, BLOCK_SIZE)[None, :]
tl.store(out_ptr, o_quant)
scales_ptr = Out_scales + token_idx + TMA_ALIGNED_MN * tl.arange(0, NUM_BLOCKS)[:, None]
tl.store(scales_ptr, o_scales)
else:
out_ptr = Out + token_idx * H + offsets
tl.store(out_ptr, gated)
@triton_op("blockwise::gated_attention", mutates_args=())
def run_gated_attention(x: torch.Tensor, gate_logits: torch.Tensor, quantize: bool = True) -> Tuple[torch.Tensor, torch.Tensor]:
b, t, h = x.shape
num_heads = gate_logits.shape[-1]
dim_head = h // num_heads
block_size = 128
num_blocks = h // block_size
num_rows = b * t
scale_max: float = 448.0
if quantize:
out = torch.empty((num_rows, h), device=x.device, dtype=torch.float8_e4m3fn)
tma_aligned_mn = get_tma_aligned_size(num_rows, 4)
scales = torch.empty_strided((num_rows, num_blocks), (1, tma_aligned_mn), dtype=torch.float, device=x.device)
else:
out = torch.empty((num_rows, h), device=x.device, dtype=torch.bfloat16)
tma_aligned_mn = 0
scales = None
wrap_triton(_gated_attention_kernel)[(num_rows,)](
x,
gate_logits,
out,
scales,
H=h,
NUM_HEADS=num_heads,
DIM_HEAD=dim_head,
NUM_BLOCKS=num_blocks,
BLOCK_SIZE=128,
TMA_ALIGNED_MN=tma_aligned_mn,
QUANTIZE=quantize,
SCALE_MAX=scale_max,
)
return out.view(b, t, h), scales
@jit
def _blockwise_dequantize_kernel(
X,
Scales,
Out,
scales_row_stride: int,
scales_col_stride: int,
H: tl.constexpr,
NUM_BLOCKS: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
token_idx = tl.program_id(0)
# Load per-block scales [NUM_BLOCKS] respecting the actual tensor strides
scales = tl.load(
Scales + token_idx * scales_row_stride + scales_col_stride * tl.arange(0, NUM_BLOCKS)
).to(tl.float32)
scales = tl.broadcast_to(scales[:, None], (NUM_BLOCKS, BLOCK_SIZE))
# Load fp8 input viewed as [NUM_BLOCKS, BLOCK_SIZE]
x_offsets = BLOCK_SIZE * tl.arange(0, NUM_BLOCKS)[:, None] + tl.arange(0, BLOCK_SIZE)[None, :]
x = tl.load(X + token_idx * H + x_offsets).to(tl.float32)
# Dequantize and store as bf16
tl.store(Out + token_idx * H + x_offsets, (x * scales).to(tl.bfloat16))
@triton_op("blockwise::dequantize", mutates_args=())
def run_blockwise_dequantize(x: torch.Tensor, scales: torch.Tensor) -> torch.Tensor:
b, s, h = x.shape
block_size = 128
num_blocks = h // block_size
num_rows = b * s
out = torch.empty((num_rows, h), device=x.device, dtype=torch.bfloat16)
wrap_triton(_blockwise_dequantize_kernel)[(num_rows,)](
x,
scales,
out,
scales_row_stride=scales.stride(0),
scales_col_stride=scales.stride(1),
H=h,
NUM_BLOCKS=num_blocks,
BLOCK_SIZE=128,
)
return out.view(b, s, h)
rowwise_int_quantize_triton = run_quantize_kernel
blockwise_quantize_adanorm_triton = run_quant_blockwise_norm_tma_aligned
blockwise_quantize_triton = run_quant_blockwise_tma_aligned
blockwise_quantize_gelu_triton = run_quant_blockwise_gelu_tma_aligned
blockwise_quantize_rms_fma_triton = run_quant_blockwise_rms_fma
rms_fma_triton = run_rms_fma
gated_attention_triton = run_gated_attention
blockwise_dequantize_triton = run_blockwise_dequantize
# # Precision-specific convenience functions for Triton kernels
# def fp8_blockwise_quantize_triton(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
# return run_quant_blockwise_tma_aligned(x, scale_max=FP8_SCALE_MAX)
# def fp7_blockwise_quantize_triton(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
# return run_quant_blockwise_tma_aligned(x, scale_max=FP7_SCALE_MAX)
# def fp6_blockwise_quantize_triton(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
# return run_quant_blockwise_tma_aligned(x, scale_max=FP6_SCALE_MAX)