# Copyright 2025 Tencent Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Pure PyTorch implementation of FP8 block-wise GEMM. This module provides CPU/Windows-compatible implementations that mirror the Triton kernel for FP8 GEMM with block-wise quantization. """ from typing import Optional import torch def fp8_gemm_torch_block( a: torch.Tensor, a_s: torch.Tensor, b: torch.Tensor, b_s: torch.Tensor, out_dtype: torch.dtype = torch.bfloat16, bias: Optional[torch.Tensor] = None, block_size: int = 128, ) -> torch.Tensor: """ Pure PyTorch implementation of FP8 GEMM with block-wise quantization. Performs a matrix multiplication using FP8 precision with per-block scaling. This implementation dequantizes the inputs and performs standard matmul. C = (A * A_scale) @ (B * B_scale).T + bias Args: a: Input activation tensor in FP8 format, shape [..., K] a_s: Scale tensor for A, shape [..., K // block_size] or [..., num_k_blocks] b: Weight tensor in FP8 format, shape [N, K] b_s: Scale tensor for B, shape [N // block_size, K // block_size] out_dtype: Output data type (default: bfloat16) bias: Optional bias tensor, shape [N] block_size: Block size used for quantization (default: 128) Returns: Output tensor of shape [..., N] """ assert a.is_contiguous() and b.is_contiguous() assert a_s.is_contiguous() and b_s.is_contiguous() K = a.size(-1) orig_shape = a.shape[:-1] M = a.numel() // K N = b.size(0) # Reshape for computation a_2d = a.view(M, K) # [M, K] # Dequantize A: expand scales to match tensor dimensions # a_s shape is typically [M, K//block_size] a_s_2d = a_s.view(M, -1) # [M, num_k_blocks] # Dequantize by expanding scales a_dq = _dequantize_per_group(a_2d, a_s_2d, block_size, K) # Dequantize B: b_s is [N//block_size, K//block_size] b_dq = _dequantize_blockwise_2d(b, b_s, block_size) # Perform matmul: [M, K] @ [K, N] -> [M, N] c = torch.matmul(a_dq.to(out_dtype), b_dq.to(out_dtype).t()) # Reshape output c = c.view(*orig_shape, N) if bias is not None: c = c + bias return c def _dequantize_per_group( x: torch.Tensor, s: torch.Tensor, group_size: int, K: int, ) -> torch.Tensor: """ Dequantize tensor with per-group scales. Args: x: Quantized tensor [M, K] s: Scale tensor [M, num_groups] group_size: Size of each group K: Total size of last dimension Returns: Dequantized tensor [M, K] """ M = x.shape[0] num_groups = s.shape[1] x_float = x.to(torch.float32) # Expand scales to match K dimension # s: [M, num_groups] -> [M, K] s_expanded = s.unsqueeze(-1).expand(M, num_groups, group_size) s_expanded = s_expanded.reshape(M, num_groups * group_size) # Handle case where K is not exactly num_groups * group_size if s_expanded.shape[1] > K: s_expanded = s_expanded[:, :K] elif s_expanded.shape[1] < K: # Pad with last scale value pad_size = K - s_expanded.shape[1] s_expanded = torch.nn.functional.pad(s_expanded, (0, pad_size), mode="replicate") return x_float * s_expanded def _dequantize_blockwise_2d( x: torch.Tensor, s: torch.Tensor, block_size: int, ) -> torch.Tensor: """ Dequantize 2D tensor with block-wise scales. Args: x: Quantized tensor [N, K] s: Scale tensor [n_blocks, k_blocks] block_size: Block size Returns: Dequantized tensor [N, K] """ N, K = x.shape n_blocks, k_blocks = s.shape x_float = x.to(torch.float32) y = torch.empty_like(x_float) for nb in range(n_blocks): n_start = nb * block_size n_end = min(n_start + block_size, N) for kb in range(k_blocks): k_start = kb * block_size k_end = min(k_start + block_size, K) scale = s[nb, kb] y[n_start:n_end, k_start:k_end] = x_float[n_start:n_end, k_start:k_end] * scale return y def fp8_gemm_torch_simple( a: torch.Tensor, a_s: torch.Tensor, b: torch.Tensor, b_s: torch.Tensor, out_dtype: torch.dtype = torch.bfloat16, bias: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Simplified PyTorch FP8 GEMM using full dequantization. This version is simpler but may use more memory for large tensors. Args: a: Input activation tensor in FP8 format a_s: Scale tensor for A b: Weight tensor in FP8 format b_s: Scale tensor for B out_dtype: Output data type bias: Optional bias tensor Returns: Output tensor """ K = a.size(-1) orig_shape = a.shape[:-1] M = a.numel() // K N = b.size(0) # Reshape a_2d = a.view(M, K) a_s_2d = a_s.view(M, -1) # Simple dequantization: repeat scales to match dimensions block_size = K // a_s_2d.shape[1] if a_s_2d.shape[1] > 0 else K # Dequantize A a_dq = a_2d.to(torch.float32) if a_s_2d.shape[1] > 1: a_s_expanded = a_s_2d.repeat_interleave(block_size, dim=1)[:, :K] a_dq = a_dq * a_s_expanded else: a_dq = a_dq * a_s_2d # Dequantize B b_dq = _dequantize_blockwise_2d(b, b_s, block_size) # Matmul c = torch.matmul(a_dq.to(out_dtype), b_dq.to(out_dtype).t()) c = c.view(*orig_shape, N) if bias is not None: c = c + bias return c