# Copyright (c) 2025 PaddlePaddle Authors. 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. # The file has been adapted from DeepSeek DeepGEMM project # Copyright (c) 2025 DeepSeek # Licensed under the MIT License - https://github.com/deepseek-ai/DeepGEMM/blob/main/LICENSE from cuda.bindings import nvrtc print(f"NVRTC version: {nvrtc.nvrtcVersion()[1:]}") import random from deep_gemm import calc_diff, ceil_div, get_col_major_tma_aligned_tensor from deep_gemm.jit_kernels.utils import get_m_alignment_for_contiguous_layout import paddle from paddle import Tensor from paddle.incubate.fp8 import deep_gemm def per_token_cast_to_fp8(x: Tensor) -> tuple[Tensor, Tensor]: assert x.dim() == 2 m, n = x.shape pad_size = (128 - (n % 128)) % 128 x = ( paddle.nn.functional.pad(x, (0, pad_size), value=0) if pad_size > 0 else x ) x_view = paddle.view(x, (m, -1, 128)) x_abs = paddle.abs(x_view).astype(paddle.float32) x_amax = paddle.amax(x_abs, axis=2) x_amax = paddle.view(x_amax, (m, -1)) x_amax = paddle.clip(x_amax, min=1e-4) scaled_x = x_view * (448.0 / x_amax.unsqueeze(2)) scaled_x_converted = paddle.view( scaled_x.astype(paddle.float8_e4m3fn), (m, n + pad_size) )[:, :n] x_amax_scaled = paddle.view((x_amax / 448.0), (m, -1)) result = (scaled_x_converted, x_amax_scaled) return result def per_block_cast_to_fp8(x: Tensor) -> tuple[Tensor, Tensor]: assert x.dim() == 2 m, n = x.shape x_padded = paddle.zeros( (ceil_div(m, 128) * 128, ceil_div(n, 128) * 128), dtype=x.dtype ) x_padded[:m, :n] = x x_view = paddle.view(x_padded, (-1, 128, x_padded.shape[1] // 128, 128)) x_abs = paddle.abs(x_view).astype(paddle.float32) x_amax = paddle.amax(x_abs, axis=(1, 3), keepdim=True) x_amax = paddle.clip(x_amax, min=1e-4) x_scaled = (x_view * (448.0 / x_amax)).astype(paddle.float8_e4m3fn) return x_scaled.view_as(x_padded)[:m, :n].contiguous(), ( paddle.view(x_amax / 448.0, (x_view.shape[0], x_view.shape[2])) ) def construct( m: int, k: int, n: int ) -> tuple[tuple[Tensor, Tensor], tuple[Tensor, Tensor], Tensor, Tensor]: x = paddle.randn((m, k), dtype=paddle.bfloat16) y = paddle.randn((n, k), dtype=paddle.bfloat16) out = paddle.empty((m, n), dtype=paddle.bfloat16) ref_out = x @ y.t() x_fp8, y_fp8 = per_token_cast_to_fp8(x), per_block_cast_to_fp8(y) # Transpose earlier so that the testing will not trigger transposing kernels x_fp8 = (x_fp8[0], get_col_major_tma_aligned_tensor(x_fp8[1])) return x_fp8, y_fp8, out, ref_out def construct_contiguous_grouped( num_groups: int, expected_m_per_group: int, k: int, n: int ) -> tuple[ int, tuple[Tensor, Tensor], tuple[Tensor, Tensor], Tensor, Tensor, Tensor ]: alignment = get_m_alignment_for_contiguous_layout() group_ms = [ int(expected_m_per_group * random.uniform(0.7, 1.3)) for _ in range(num_groups) ] m = sum([ceil_div(x, alignment) * alignment for x in group_ms]) x = paddle.randn((m, k), dtype=paddle.bfloat16) y = paddle.randn((num_groups, n, k), dtype=paddle.bfloat16) m_indices = paddle.empty([m], dtype=paddle.int32) out = paddle.empty((m, n), dtype=paddle.bfloat16) ref_out = paddle.randn((m, n), dtype=paddle.bfloat16) start = 0 for i, group_m in enumerate(group_ms): actual_end = start + group_m aligned_end = start + ceil_div(group_m, alignment) * alignment m_indices[start:actual_end] = i m_indices[actual_end:aligned_end] = -1 ref_out[start:aligned_end] = x[start:aligned_end] @ y[i].t() start = aligned_end ref_out = paddle.where( (m_indices == -1).unsqueeze(1), paddle.zeros_like(ref_out), ref_out ) assert m % 4 == 0, f"TMA alignment error: {m}" x_fp8 = per_token_cast_to_fp8(x) y_fp8 = ( paddle.empty_like(y, dtype=paddle.float8_e4m3fn), paddle.empty( (num_groups, ceil_div(n, 128), k // 128), dtype=paddle.float32 ), ) for i in range(num_groups): # y_fp8[0][i], y_fp8[1][i] = per_block_cast_to_fp8(y[i]) y_fp8_0_i, y_fp8_1_i = per_block_cast_to_fp8(y[i]) paddle.assign(y_fp8_0_i, y_fp8[0][i]) paddle.assign(y_fp8_1_i, y_fp8[1][i]) return m, x_fp8, y_fp8, m_indices, out, ref_out def construct_masked_grouped( num_groups: int, max_m: int, expected_m_per_group: int, k: int, n: int ) -> tuple[ tuple[Tensor, Tensor], tuple[Tensor, Tensor], Tensor, Tensor, Tensor ]: x = paddle.randn((num_groups, max_m, k), dtype=paddle.bfloat16) y = paddle.randn((num_groups, n, k), dtype=paddle.bfloat16) out = paddle.empty((num_groups, max_m, n), dtype=paddle.bfloat16) ref_out = paddle.einsum("gmk,gnk->gmn", x, y) assert max_m % 4 == 0, f"TMA alignment error: {max_m}" x_fp8 = ( paddle.empty_like(x, dtype=paddle.float8_e4m3fn), paddle.empty((num_groups, max_m, k // 128), dtype=paddle.float32), ) y_fp8 = ( paddle.empty_like(y, dtype=paddle.float8_e4m3fn), paddle.empty( (num_groups, ceil_div(n, 128), k // 128), dtype=paddle.float32 ), ) for i in range(num_groups): # x_fp8[0][i], x_fp8[1][i] = per_token_cast_to_fp8(x[i]) # y_fp8[0][i], y_fp8[1][i] = per_block_cast_to_fp8(y[i]) x_fp8_0_i, x_fp8_1_i = per_token_cast_to_fp8(x[i]) paddle.assign(x_fp8_0_i, x_fp8[0][i]) paddle.assign(x_fp8_1_i, x_fp8[1][i]) y_fp8_0_i, y_fp8_1_i = per_block_cast_to_fp8(y[i]) paddle.assign(y_fp8_0_i, y_fp8[0][i]) paddle.assign(y_fp8_1_i, y_fp8[1][i]) # Transpose earlier so that the testing will not trigger transposing kernels x_fp8 = (x_fp8[0], get_col_major_tma_aligned_tensor(x_fp8[1])) # Construct mask masked_m = paddle.empty((num_groups,), dtype=paddle.int32) for j in range(num_groups): masked_m[j] = int(expected_m_per_group * random.uniform(0.7, 1.3)) assert masked_m.amax().item() <= max_m return x_fp8, y_fp8, masked_m, out, ref_out def construct_wgrad( m: int, k: int, n: int ) -> tuple[ tuple[Tensor, Tensor], tuple[Tensor, Tensor], Tensor, Tensor, Tensor ]: x = paddle.randn((m, k), dtype=paddle.bfloat16) y = paddle.randn((n, k), dtype=paddle.bfloat16) residual = paddle.randn((m, n), dtype=paddle.float32) * 10 out = residual.clone() ref_out = residual + ( x.astype(paddle.float32) @ y.astype(paddle.float32).t() ) x_fp8 = per_token_cast_to_fp8(x) y_fp8 = per_token_cast_to_fp8(y) # NOTES: please do inplace add on the `out` later return x_fp8, y_fp8, residual, out, ref_out def construct_k_grouped_wgrad( m: int, n: int, k_sizes: list[int] ) -> tuple[ tuple[Tensor, Tensor], tuple[Tensor, Tensor], Tensor, Tensor, list[int] ]: num_groups, total_k = len(k_sizes), sum(k_sizes) x_flat = paddle.empty((m * total_k,), dtype=paddle.bfloat16) y_flat = paddle.empty((n * total_k,), dtype=paddle.bfloat16) out = paddle.zeros((num_groups, m, n), dtype=paddle.float32) ref_out = paddle.zeros((num_groups, m, n), dtype=paddle.float32) # Fill tensors with data and compute reference output x_offset, y_offset = 0, 0 for idx, k in enumerate(k_sizes): x_chunk = paddle.randn((m, k), dtype=paddle.bfloat16) y_chunk = paddle.randn((n, k), dtype=paddle.bfloat16) paddle.assign( x_chunk.flatten(), output=x_flat[x_offset : x_offset + m * k] ) paddle.assign( y_chunk.flatten(), output=y_flat[y_offset : y_offset + n * k] ) ref_out[idx] = ( x_chunk.astype(paddle.float32) @ y_chunk.astype(paddle.float32).t() ) x_offset += m * k y_offset += n * k x_fp8_flat = paddle.empty_like(x_flat, dtype=paddle.float8_e4m3fn) y_fp8_flat = paddle.empty_like(y_flat, dtype=paddle.float8_e4m3fn) total_scale_factors = sum(ceil_div(k, 128) for k in k_sizes) x_scales = paddle.empty((total_scale_factors, m), dtype=paddle.float32) y_scales = paddle.empty((total_scale_factors, n), dtype=paddle.float32) # Cast to FP8 and prepare scale factors x_offset, y_offset, scale_offset = 0, 0, 0 for k in k_sizes: x_fp8_chunk, x_scale_chunk = per_token_cast_to_fp8( paddle.view(x_flat[x_offset : x_offset + m * k], (m, k)) ) y_fp8_chunk, y_scale_chunk = per_token_cast_to_fp8( paddle.view(y_flat[y_offset : y_offset + n * k], (n, k)) ) paddle.assign( x_fp8_chunk.flatten(), output=x_fp8_flat[x_offset : x_offset + m * k], ) paddle.assign( y_fp8_chunk.flatten(), output=y_fp8_flat[y_offset : y_offset + n * k], ) num_scales = ceil_div(k, 128) paddle.assign( x_scale_chunk.T, output=x_scales[scale_offset : scale_offset + num_scales], ) paddle.assign( y_scale_chunk.T, output=y_scales[scale_offset : scale_offset + num_scales], ) x_offset += m * k y_offset += n * k scale_offset += num_scales return (x_fp8_flat, x_scales), (y_fp8_flat, y_scales), out, ref_out, k_sizes def test_gemm() -> None: print("Testing GEMM:") for m in (64, 128, 4096): for k, n in [ (576, 7168), (7168, 2112), (1536, 24576), (512, 32768), (16384, 7168), (7168, 4096), (2048, 7168), ]: x_fp8, y_fp8, out, ref_out = construct(m, k, n) deep_gemm.gemm_fp8_fp8_bf16_nt(x_fp8, y_fp8, out) diff = calc_diff(out, ref_out) print("diff:", diff) assert diff < 0.001, f"{m=}, {k=}, {n=}, {diff:.5f}" print() def test_m_grouped_gemm_contiguous() -> None: print("Testing grouped contiguous GEMM:") for num_groups, expected_m_per_group, k, n in ( (4, 8192, 7168, 4096), (4, 8192, 2048, 7168), (8, 4096, 7168, 4096), (8, 4096, 2048, 7168), (32, 256, 7168, 4096), (32, 256, 2048, 7168), ): # NOTES: we should mask the unfilled part before calculating difference m, x_fp8, y_fp8, m_indices, out, ref_out = construct_contiguous_grouped( num_groups, expected_m_per_group, k, n ) deep_gemm.m_grouped_gemm_fp8_fp8_bf16_nt_contiguous( x_fp8, y_fp8, out, m_indices ) out = paddle.where( (m_indices == -1).unsqueeze(1), paddle.zeros_like(out), out ) diff = calc_diff(out, ref_out) print("diff:", diff) assert diff < 0.001, f"{m=}, {k=}, {n=}, {diff:.5f}" print() def test_m_grouped_gemm_masked() -> None: print("Testing grouped masked GEMM:") for num_groups, expected_m_per_group in ((1, 1024), (2, 512), (4, 256)): for k, n in ( (7168, 4096), (2048, 7168), ): # Test correctness for i in range(10): x_fp8, y_fp8, masked_m, out, ref_out = construct_masked_grouped( num_groups, 4096, expected_m_per_group, k, n ) deep_gemm.m_grouped_gemm_fp8_fp8_bf16_nt_masked( x_fp8, y_fp8, out, masked_m, expected_m_per_group ) for j in range(num_groups): diff = calc_diff( out[j, : masked_m[j].item()], ref_out[j, : masked_m[j].item()], ) assert diff < 0.001, ( f"{expected_m_per_group=}, {k=}, {n=}, {j=}, masked_m={masked_m[j]}, {num_groups=}, {diff:.5f}" ) # noinspection PyShadowingNames # def test_func(): # deep_gemm.m_grouped_gemm_fp8_fp8_bf16_nt_masked(x_fp8, y_fp8, out, masked_m, expected_m_per_group) # Test performance with fixed shapes # noinspection PyUnboundLocalVariable # valid_m = masked_m.sum().item() # t = bench_kineto(test_func, "fp8_gemm", suppress_kineto_output=True) # print(f" > Perf ({num_groups=}, expected_m_per_group={expected_m_per_group:4}, n={n:4}, k={k:4}): {t * 1e6:4.0f} us | " # f"throughput: {2 * valid_m * n * k / t / 1e12:4.0f} TFLOPS, " # f"{(valid_m * k + num_groups * k * n + valid_m * n * 2) / 1e9 / t:4.0f} GB/s") print() def test_wgrad_gemm(): print("Testing weight gradient GEMM:") for k in (4096, 8192): for m, n in ( (7168, 2112), (1536, 24576), (512, 32768), (16384, 7168), (7168, 4096), (2048, 7168), ): # Test correctness x_fp8, y_fp8, residual, out, ref_out = construct_wgrad(m, k, n) deep_gemm.wgrad_gemm_fp8_fp8_fp32_nt(x_fp8, y_fp8, out) diff = calc_diff(out, ref_out) print("diff:", diff) assert diff < 0.001, f"{m=}, {k=}, {n=}, {diff:.5f}" print() def test_k_grouped_wgrad_gemm(): print("Testing grouped weight gradient GEMM:") for num_groups, base_k in ((4, 4096), (4, 8192), (8, 4096)): for m, n in ((7168, 4096), (2048, 7168)): # Vary k sizes around base_k k_sizes = [ base_k + random.randint(-1, 1) * 128 for _ in range(num_groups - 1) ] k_sizes.append(base_k * num_groups - sum(k_sizes)) # Test correctness x_fp8, y_fp8, out, ref_out, k_sizes = construct_k_grouped_wgrad( m, n, k_sizes ) deep_gemm.k_grouped_wgrad_gemm_fp8_fp8_fp32_nt( x_fp8, y_fp8, out, k_sizes ) for idx in range(num_groups): diff = calc_diff(out[idx], ref_out[idx]) print("diff:", diff) assert diff < 0.001, ( f"{num_groups=}, {m=}, {n=}, k={k_sizes[idx]}, batch={idx}, {diff:.5f}" ) print() if __name__ == "__main__": paddle.seed(0) random.seed(0) print("Library path:") print(f" > {deep_gemm.__path__}\n") test_gemm() test_m_grouped_gemm_contiguous() test_m_grouped_gemm_masked() test_wgrad_gemm() test_k_grouped_wgrad_gemm()