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
275 lines
9.8 KiB
Python
275 lines
9.8 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.
|
|
|
|
import logging
|
|
|
|
import tokenspeed_kernel
|
|
import torch
|
|
from tokenspeed_kernel.ops.quantization.flashinfer import fp4_quantize
|
|
from torch.nn.parameter import Parameter
|
|
|
|
from tokenspeed.runtime.layers.quantization.base_config import QuantizeMethodBase
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _pdl_enabled() -> bool:
|
|
from tokenspeed.runtime.utils.pdl import pdl_enabled
|
|
|
|
return pdl_enabled()
|
|
|
|
|
|
class Nvfp4LinearMethod(QuantizeMethodBase):
|
|
"""Linear method for NVFP4 quantization.
|
|
|
|
Weight structure:
|
|
- weight: uint8 [output_size, input_size // 2] (packed FP4)
|
|
- weight_scale: float8_e4m3fn [output_size, input_size // group_size]
|
|
- weight_scale_2: float32 scalar (per-tensor)
|
|
- input_scale: float32 scalar (per-tensor)
|
|
"""
|
|
|
|
def __init__(self, quant_config):
|
|
self.quant_config = quant_config
|
|
self.group_size = quant_config.group_size
|
|
|
|
def create_weights(
|
|
self,
|
|
layer: torch.nn.Module,
|
|
input_size_per_partition: int,
|
|
output_partition_sizes: list[int],
|
|
input_size: int,
|
|
output_size: int,
|
|
params_dtype: torch.dtype,
|
|
**extra_weight_attrs,
|
|
):
|
|
output_size_per_partition = sum(output_partition_sizes)
|
|
weight_loader = extra_weight_attrs.get("weight_loader")
|
|
layer.logical_widths = output_partition_sizes
|
|
layer.input_size_per_partition = input_size_per_partition
|
|
layer.output_size_per_partition = output_size_per_partition
|
|
|
|
# FP4 packed weight: 2 values per byte, input_dim halved
|
|
weight = Parameter(
|
|
torch.empty(
|
|
output_size_per_partition,
|
|
input_size_per_partition // 2,
|
|
dtype=torch.uint8,
|
|
),
|
|
requires_grad=False,
|
|
)
|
|
# Set attributes for TP sharding in weight_loader
|
|
weight.output_dim = 0
|
|
weight.input_dim = 1
|
|
if weight_loader:
|
|
weight.weight_loader = weight_loader
|
|
layer.register_parameter("weight", weight)
|
|
|
|
# Block scales: one per group_size elements
|
|
weight_scale = Parameter(
|
|
torch.empty(
|
|
output_size_per_partition,
|
|
input_size_per_partition // self.group_size,
|
|
dtype=torch.float8_e4m3fn,
|
|
),
|
|
requires_grad=False,
|
|
)
|
|
weight_scale.output_dim = 0
|
|
weight_scale.input_dim = 1
|
|
if weight_loader:
|
|
weight_scale.weight_loader = weight_loader
|
|
layer.register_parameter("weight_scale", weight_scale)
|
|
|
|
# Per-tensor scales: scalar per partition, use needs_scalar_to_array for fused loading
|
|
input_scale = Parameter(
|
|
torch.full(
|
|
(len(output_partition_sizes),),
|
|
torch.finfo(torch.float32).min,
|
|
dtype=torch.float32,
|
|
),
|
|
requires_grad=False,
|
|
)
|
|
input_scale.needs_scalar_to_array = True
|
|
if weight_loader:
|
|
input_scale.weight_loader = weight_loader
|
|
layer.register_parameter("input_scale", input_scale)
|
|
|
|
weight_scale_2 = Parameter(
|
|
torch.full(
|
|
(len(output_partition_sizes),),
|
|
torch.finfo(torch.float32).min,
|
|
dtype=torch.float32,
|
|
),
|
|
requires_grad=False,
|
|
)
|
|
weight_scale_2.needs_scalar_to_array = True
|
|
if weight_loader:
|
|
weight_scale_2.weight_loader = weight_loader
|
|
layer.register_parameter("weight_scale_2", weight_scale_2)
|
|
|
|
def process_weights_after_loading(self, layer):
|
|
"""Compute alpha and input_scale_inv, swizzle block scales."""
|
|
logger.debug(
|
|
"[FP4_DENSE_POSTLOAD] w=%s(%s) ws=%s is=%s ws2=%s",
|
|
layer.weight.shape,
|
|
layer.weight.dtype,
|
|
layer.weight_scale.shape,
|
|
layer.input_scale,
|
|
layer.weight_scale_2,
|
|
)
|
|
input_scale = layer.input_scale.max().to(torch.float32)
|
|
weight_scale_2 = layer.weight_scale_2.max().to(torch.float32)
|
|
layer.input_scale = Parameter(input_scale, requires_grad=False)
|
|
layer.weight_scale_2 = Parameter(weight_scale_2, requires_grad=False)
|
|
layer.alpha = Parameter(input_scale * weight_scale_2, requires_grad=False)
|
|
layer.input_scale_inv = Parameter(
|
|
(1.0 / input_scale).to(torch.float32), requires_grad=False
|
|
)
|
|
if layer.interleave_linear_and_gate:
|
|
gate_weight, linear_weight = layer.weight.chunk(2, dim=0)
|
|
linear_gate_weight = torch.cat((linear_weight, gate_weight), dim=0)
|
|
layer.weight_swiglu_interleaved = Parameter(
|
|
interleave_linear_and_gate(
|
|
linear_gate_weight,
|
|
group_size=64,
|
|
dim=0,
|
|
),
|
|
requires_grad=False,
|
|
)
|
|
# layer.weight_scale is the canonical unswizzled [N, K/group]
|
|
# tensor. Reorder gate/linear first, then swizzle for the CUTE
|
|
# kernel; chunking layer.weight_scale_interleaved would be wrong.
|
|
gate_scale, linear_scale = layer.weight_scale.chunk(2, dim=0)
|
|
linear_gate_scale = torch.cat((linear_scale, gate_scale), dim=0)
|
|
layer.weight_scale_swiglu_interleaved = Parameter(
|
|
swizzle_blockscale_2d(
|
|
interleave_linear_and_gate(
|
|
linear_gate_scale,
|
|
group_size=64,
|
|
dim=0,
|
|
)
|
|
),
|
|
requires_grad=False,
|
|
)
|
|
del layer.weight
|
|
del layer.weight_scale
|
|
else:
|
|
# Swizzle block scales for CUTLASS
|
|
layer.weight_scale_interleaved = Parameter(
|
|
swizzle_blockscale_2d(layer.weight_scale),
|
|
requires_grad=False,
|
|
)
|
|
del layer.weight_scale
|
|
|
|
def apply(self, layer, x, bias=None):
|
|
"""Forward pass: quantize input to FP4, run FP4 GEMM.
|
|
|
|
``x`` may be either a bf16/fp16 activation tensor (normal path) or a
|
|
pre-quantized ``(x_fp4, x_scale)`` tuple.
|
|
"""
|
|
w_n = layer.output_size_per_partition
|
|
|
|
if isinstance(x, tuple):
|
|
# Pre-quantized path: no fp4_quantize launch. Output dtype is bf16.
|
|
x_fp4, x_scale = x
|
|
output_dtype = torch.bfloat16
|
|
|
|
else:
|
|
x_fp4, x_scale = fp4_quantize(
|
|
x, layer.input_scale_inv, enable_pdl=_pdl_enabled()
|
|
)
|
|
output_dtype = x.dtype
|
|
|
|
kernel_override = layer.override_kernel_name
|
|
out = tokenspeed_kernel.mm(
|
|
x_fp4,
|
|
layer.weight.T,
|
|
A_scales=x_scale,
|
|
B_scales=layer.weight_scale_interleaved.T,
|
|
bias=bias,
|
|
out_dtype=output_dtype,
|
|
alpha=layer.alpha,
|
|
quant="nvfp4",
|
|
enable_pdl=_pdl_enabled(),
|
|
override=kernel_override,
|
|
expected_kernel_name=kernel_override or "cublaslt_mm_nvfp4",
|
|
)
|
|
return out.view(x_fp4.size(0), w_n)
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Utilities for FP4 linear method
|
|
# -------------------------------------------------------------------------
|
|
|
|
|
|
def swizzle_blockscale_2d(scales):
|
|
"""Swizzle 2D FP8 block scales for CUTLASS."""
|
|
M, K = scales.shape
|
|
|
|
def round_up(x, m):
|
|
return (x + m - 1) // m * m
|
|
|
|
M_padded = round_up(M, 128)
|
|
K_padded = round_up(K, 4)
|
|
padded = torch.zeros((M_padded, K_padded), dtype=scales.dtype, device=scales.device)
|
|
padded[:M, :K] = scales
|
|
rows, cols = padded.shape
|
|
padded = padded.reshape(rows // 128, 4, 32, cols // 4, 4)
|
|
padded = padded.permute((0, 3, 2, 1, 4))
|
|
return padded.contiguous().reshape(M_padded, K_padded)
|
|
|
|
|
|
def interleave_linear_and_gate(
|
|
tensor: torch.Tensor,
|
|
group_size: int = 64,
|
|
dim: int = 0,
|
|
) -> torch.Tensor:
|
|
"""Interleave ``[linear all][gate all]`` as ``[linear chunk][gate chunk]``.
|
|
|
|
This matches the FC1 GEMM+SwiGLU preprocessing layout expected by
|
|
``cute_dsl_nvfp4_dense_gemm_swiglu_blackwell``.
|
|
"""
|
|
if tensor.ndim == 0:
|
|
raise ValueError("expected a tensor with at least one dimension")
|
|
|
|
dim = dim % tensor.ndim
|
|
sizes = tensor.size()
|
|
dim_size = sizes[dim]
|
|
if dim_size % (group_size * 2) != 0:
|
|
raise ValueError(
|
|
f"dimension {dim} size {dim_size} must be divisible by "
|
|
f"2 * group_size={2 * group_size}"
|
|
)
|
|
|
|
prev_sizes = sizes[:dim]
|
|
post_sizes = sizes[dim + 1 :]
|
|
return (
|
|
tensor.reshape(
|
|
*prev_sizes,
|
|
2,
|
|
dim_size // (group_size * 2),
|
|
group_size,
|
|
*post_sizes,
|
|
)
|
|
.transpose(dim, dim + 1)
|
|
.reshape(*sizes)
|
|
.contiguous()
|
|
)
|