NVIDIA / NVIDIA/cutlass

[QST] FP8 Blockwise GEMM worse than fp16 case

Open
#2,923 15 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

? - Needs Triage inactive-30d inactive-90d question
Dominant language
C++
Stars
10.5k
Forks
2.1k
Avg merge
3d 11h
Merged PRs (30d)
7

Description

A FlashInfer user reported that FP8 blockwise GEMM is slower than FP16 PyTorch on B200 (issue link). I’ve been able to reproduce this with their benchmark script:

  Out    In  Batch |        FP16        |            FP8 Cutlass            |            FP8 TRT-LLM           
                   | Median(us)  Std(%) | Median(us)  Std(%)  Ratio to FP16 | Median(us)  Std(%)  Ratio to FP16
-------------------------------------------------------------------------------------------------------------------------------
 2048  1024      8 |       5.02    2.81 |       7.62    1.62           0.66 |       5.15    2.39           0.98
 2048  1024     32 |       4.86    2.03 |       7.60    1.99           0.64 |       5.22    2.94           0.93
 2048  1024     64 |       4.93    2.03 |       7.62    1.57           0.65 |       4.96    1.82           0.99
 2048  1024    128 |       5.06    1.93 |       7.71    1.88           0.66 |       4.99    1.96           1.01
 2048  1024    256 |       5.26    2.16 |       7.52    1.56           0.70 |       5.09    2.29           1.03
 2048  1024    512 |       5.92    2.68 |       7.68    1.84           0.77 |       8.67    1.23           0.68
 2048  1024   1024 |       7.14    1.69 |       8.00    1.55           0.89 |      16.19    1.36           0.44
 2048   128      8 |       3.06    3.98 |       5.57    2.19           0.55 |          -       -              -
 2048   128     32 |       3.04    4.47 |       5.60    2.46           0.54 |          -       -              -
 2048   128     64 |       2.98    3.54 |       5.63    2.24           0.53 |          -       -              -
 2048   128    128 |       3.04    4.06 |       5.66    2.54           0.54 |          -       -              -
 2048   128    256 |       3.36    3.13 |       5.54    1.98           0.61 |          -       -              -
 2048   128    512 |       3.74    3.27 |       5.57    1.84           0.67 |          -       -              -
 2048   128   1024 |       3.76    3.33 |       5.66    1.98           0.66 |          -       -              -
  256  2048      8 |       6.08    9.63 |       9.36    2.31           0.65 |       6.38    2.70           0.95
  256  2048     32 |       6.50    2.19 |       9.41    2.08           0.69 |       6.37    2.40           1.02
  256  2048     64 |       6.53    2.62 |       9.36    2.63           0.70 |       6.21    1.66           1.05
  256  2048    128 |       6.56    2.37 |       9.44    2.69           0.69 |       6.21    1.44           1.06
  256  2048    256 |       5.28    2.05 |       9.22    1.45           0.57 |       6.21    1.87           0.85
  256  2048    512 |       5.70    2.20 |       9.25    1.53           0.62 |       6.24    1.93           0.91
  256  2048   1024 |       6.11    2.13 |       9.38    1.29           0.65 |       6.43    1.66           0.95
 2560  2048      8 |       6.46    2.08 |       9.86    1.28           0.66 |       7.01    1.86           0.92
 2560  2048     32 |       6.50    2.21 |       9.89    1.46           0.66 |       7.04    2.03           0.92
 2560  2048     64 |       6.54    1.82 |       9.92    1.43           0.66 |       7.01    1.73           0.93
 2560  2048    128 |       7.01    1.74 |       9.98    1.22           0.70 |       7.01    1.75           1.00
 2560  2048    256 |       7.30    2.11 |      10.24    1.72           0.71 |      11.26    0.92           0.65
 2560  2048    512 |       8.61    1.54 |      10.43    1.64           0.83 |      16.67    0.74           0.52
 2560  2048   1024 |      11.04    1.48 |      15.42    1.52           0.72 |      27.10    0.79           0.41
 3072  2048      8 |       7.04    2.23 |      10.05    1.72           0.70 |       7.10    1.36           0.99
 3072  2048     32 |       6.72    2.14 |       9.95    1.48           0.68 |       7.10    1.73           0.95
 3072  2048     64 |       6.85    1.77 |       9.98    1.23           0.69 |       7.07    1.88           0.97
 3072  2048    128 |       7.23    2.18 |      10.08    1.17           0.72 |       7.17    1.48           1.01
 3072  2048    256 |       7.50    1.79 |      10.34    1.46           0.73 |      11.90    1.06           0.63
 3072  2048    512 |       9.31    1.67 |      10.56    1.34           0.88 |      17.38    0.79           0.54
 3072  2048   1024 |      13.22    1.14 |      16.10    1.27           0.82 |      31.17    0.83           0.42

(ignore fp8 trt-llm)

Our implementation is fairly similar to the cutlass example: https://github.com/NVIDIA/cutlass/blob/main/examples/81_blackwell_gemm_blockwise/81_blackwell_gemm_blockwise.cu - the only difference I see is the ScaleConfig.

I'm still learning so could you lmk if my understanding is incorrect: FP8 blockwise GEMM should be able to outperform FP16 on B200 (at least on some regimes) since B200 has higher FP8 tensor core flops/s and fp8 requires less memory bandwidth. There are additional overheads (extra DRAM loads for A/B scales, additional indexing math, and extra ops for scaling/casting), but FP8 to be able to win - at least for regimes where the tensor core compute or memory bandwidth dominates.

I'm taking a look now - but wanted to check in with you guys if you had any pointers on how to approach this. If I find a clear improvement, I’m happy to update the CUTLASS example as well if that’s useful.

cc @hwu36

Additional details
Some ncu metrics - pytorch vs cutlass (Click to expand)
pytorch --shapes 2560x2048 --batch-sizes 256
15.01 us
Grid Size	128
Cluster Size	4
Block Size	256

Compute (SM) Throughput [%]	23.98
Duration [us]	15.01
Memory Throughput [%]	18.91
Elapsed Cycles [cycle]	15653
L1/TEX Cache Throughput [%]	36.36
SM Active Cycles [cycle]	7918.57
L2 Cache Throughput [%]	18.91
SM Frequency [Ghz]	1.04
DRAM Throughput [%]	11.53
DRAM Frequency [Ghz]	3.97

Theoretical Occupancy [%]	12.50
Theoretical Active Warps per SM [warp]	8
Achieved Occupancy [%]	9.22
Achieved Active Warps Per SM [warp]	5.90
Cluster Occupancy [%]	2.79
Max Active Clusters [cluster]	33
Overall GPU Occupancy [%]	0.35
	
cutlass --shapes 2560x2048 --batch-sizes 256
18.46us
Grid Size	40
Cluster Size	0
Block Size	384

Compute (SM) Throughput [%]	6.66
Duration [us]	18.46
Memory Throughput [%]	5.51
Elapsed Cycles [cycle]	20781
L1/TEX Cache Throughput [%]	28.88
SM Active Cycles [cycle]	3897.02
L2 Cache Throughput [%]	5.19
SM Frequency [Ghz]	1.12
DRAM Throughput [%]	4.77
DRAM Frequency [Ghz]	3.98

Theoretical Occupancy [%]	18.75
Theoretical Active Warps per SM [warp]	12
Achieved Occupancy [%]	12.87
Achieved Active Warps Per SM [warp]	8.24
Cluster Occupancy [%]	0
Max Active Clusters [cluster]	0
Overall GPU Occupancy [%]	0
repro script (click to expand)
"""
Combined performance benchmark comparing:
- FP16 torch.nn.functional.linear
- FP8 gemm_fp8_nt_groupwise (cutlass backend)
- FP8 gemm_fp8_nt_groupwise (trtllm backend)

Tests various shapes and batch sizes with unified reporting.
"""

import os
import sys
import torch
import numpy as np

# Set environment variables
os.environ['FLASHINFER_DISABLE_VERSION_CHECK'] = '1'

from flashinfer.testing.utils import bench_gpu_time_with_cupti, quantize_fp8

# Import FlashInfer after path setup
sys.path.insert(0, '/home/scratch.vgimpelson_ent/flashinfer')
from flashinfer.gemm import gemm_fp8_nt_groupwise


def create_fp8_tensors(m, n, k, scale_major_mode="MN"):
    """Create FP8 tensors with proper scaling for gemm_fp8_nt_groupwise."""
    block_size = 128
    
    # Create input tensors in bfloat16
    a_bf16 = torch.randn(m, k, device='cuda', dtype=torch.bfloat16)
    b_bf16 = torch.randn(n, k, device='cuda', dtype=torch.bfloat16)
    
    a_scale_shape = (k // block_size, m)
    a_tile_shape = (1, block_size)
    
    b_scale_shape = (k // block_size, n // block_size)
    b_tile_shape = (block_size, block_size)
    
    a_fp8, a_scale = quantize_fp8(a_bf16, a_scale_shape, a_tile_shape, scale_major_mode)
    b_fp8, b_scale = quantize_fp8(b_bf16, b_scale_shape, b_tile_shape, scale_major_mode)
    
    # Create output tensor
    out = torch.empty(m, n, device='cuda', dtype=torch.bfloat16)
    
    return a_fp8, b_fp8, a_scale, b_scale, out


def benchmark_fp16_linear(batch_size, out_features, in_features):
    """Benchmark FP16 torch.nn.functional.linear."""
    device = 'cuda'
    dtype = torch.float16
    
    # Create tensors
    weight = torch.randn(out_features, in_features, device=device, dtype=dtype)
    bias = torch.randn(out_features, device=device, dtype=dtype)
    input_tensor = torch.randn(batch_size, in_features, device=device, dtype=dtype)
    
    # Warmup
    for _ in range(5):
        _ = torch.nn.functional.linear(input_tensor, weight, bias)
    torch.cuda.synchronize()
    
    # Benchmark function
    def benchmark_fn():
        return torch.nn.functional.linear(input_tensor, weight, bias)
    
    # Run benchmark with CUPTI
    times = bench_gpu_time_with_cupti(
        benchmark_fn,
        l2_flush=True,
        repeat_iters=100,
    )
    
    # Calculate statistics (convert ms to us)
    times_np = np.array(times) * 1000  # ms to us
    median_time = np.median(times_np)
    std_time = np.std(times_np)
    std_pct = (std_time / median_time) * 100 if median_time > 0 else 0
    
    # Calculate FLOPS
    flops = 2 * batch_size * in_features * out_features + batch_size * out_features
    tflops_per_sec = flops / (median_time * 1e-6) / 1e12
    
    return {
        'median_us': median_time,
        'std_pct': std_pct,
        'tflops_per_sec': tflops_per_sec,
    }


def benchmark_fp8_gemm(batch_size, out_features, in_features, backend='cutlass'):
    """Benchmark FP8 gemm_fp8_nt_groupwise."""
    m, n, k = batch_size, out_features, in_features
    scale_major_mode = 'MN'
    
    # Create tensors
    a_fp8, b_fp8, a_scale, b_scale, out = create_fp8_tensors(m, n, k, scale_major_mode)
    
    # Warmup
    for _ in range(5):
        gemm_fp8_nt_groupwise(
            a_fp8, b_fp8, a_scale, b_scale,
            scale_major_mode=scale_major_mode,
            mma_sm=1,
            out=out,
            backend=backend,
        )
    torch.cuda.synchronize()
    
    # Benchmark function
    def benchmark_fn():
        return gemm_fp8_nt_groupwise(
            a_fp8, b_fp8, a_scale, b_scale,
            scale_major_mode=scale_major_mode,
            mma_sm=1,
            out=out,
            backend=backend,
        )
    
    # Run benchmark with CUPTI
    times = bench_gpu_time_with_cupti(
        benchmark_fn,
        l2_flush=True,
        repeat_iters=100,
    )
    
    # Calculate statistics (convert ms to us)
    times_np = np.array(times) * 1000  # ms to us
    median_time = np.median(times_np)
    std_time = np.std(times_np)
    std_pct = (std_time / median_time) * 100 if median_time > 0 else 0
    
    # Calculate FLOPS
    flops = 2 * m * n * k
    tflops_per_sec = flops / (median_time * 1e-6) / 1e12
    
    return {
        'median_us': median_time,
        'std_pct': std_pct,
        'tflops_per_sec': tflops_per_sec,
    }


def run_combined_benchmark():
    """Run combined benchmark for all configurations."""
    
    print("=" * 127)
    print("Combined Performance Benchmark: FP16 Linear vs FP8 GEMM (Cutlass vs TRT-LLM)")
    print("=" * 127)
    print("Configurations:")
    print("  - FP16: torch.nn.functional.linear")
    print("  - FP8 Cutlass: gemm_fp8_nt_groupwise (cutlass backend)")
    print("  - FP8 TRT-LLM: gemm_fp8_nt_groupwise (trtllm backend)")
    print("  - Benchmarking with CUPTI, L2 flush enabled")
    print("=" * 127)
    print()
    
    # Test configurations
    shapes = [
        [2048, 1024],
        [2048, 128],
        [256, 2048],
        [2560, 2048],
        [3072, 2048],
    ]
    
    batch_sizes = [128, 256, 512, 1024]
    
    results = []
    
    for out_features, in_features in shapes:
        for batch_size in batch_sizes:
            print(f"\nTesting: Batch={batch_size}, Out={out_features}, In={in_features}")
            
            result = {
                'batch': batch_size,
                'out': out_features,
                'in': in_features,
            }
            
            # Benchmark FP16
            try:
                print("  Running FP16 linear...")
                fp16_result = benchmark_fp16_linear(batch_size, out_features, in_features)
                result['fp16_median'] = fp16_result['median_us']
                result['fp16_std_pct'] = fp16_result['std_pct']
                result['fp16_tflops'] = fp16_result['tflops_per_sec']
            except Exception as e:
                print(f"  FP16 ERROR: {e}")
                result['fp16_median'] = None
                result['fp16_std_pct'] = None
                result['fp16_tflops'] = None
            
            # Benchmark FP8 Cutlass
            try:
                print("  Running FP8 Cutlass...")
                cutlass_result = benchmark_fp8_gemm(batch_size, out_features, in_features, backend='cutlass')
                result['cutlass_median'] = cutlass_result['median_us']
                result['cutlass_std_pct'] = cutlass_result['std_pct']
                result['cutlass_tflops'] = cutlass_result['tflops_per_sec']
                
                # Calculate ratio
                if result['fp16_median'] is not None:
                    result['cutlass_ratio'] = result['fp16_median'] / result['cutlass_median']
                else:
                    result['cutlass_ratio'] = None
            except Exception as e:
                print(f"  Cutlass ERROR: {e}")
                result['cutlass_median'] = None
                result['cutlass_std_pct'] = None
                result['cutlass_tflops'] = None
                result['cutlass_ratio'] = None
            
            # Benchmark FP8 TRT-LLM
            try:
                print("  Running FP8 TRT-LLM...")
                trtllm_result = benchmark_fp8_gemm(batch_size, out_features, in_features, backend='trtllm')
                result['trtllm_median'] = trtllm_result['median_us']
                result['trtllm_std_pct'] = trtllm_result['std_pct']
                result['trtllm_tflops'] = trtllm_result['tflops_per_sec']
                
                # Calculate ratio
                if result['fp16_median'] is not None:
                    result['trtllm_ratio'] = result['fp16_median'] / result['trtllm_median']
                else:
                    result['trtllm_ratio'] = None
            except Exception as e:
                print(f"  TRT-LLM ERROR: {e}")
                result['trtllm_median'] = None
                result['trtllm_std_pct'] = None
                result['trtllm_tflops'] = None
                result['trtllm_ratio'] = None
            
            results.append(result)
    
    # Print summary table
    print("\n" + "=" * 127)
    print("SUMMARY TABLE")
    print("=" * 127)
    # First header line with column categories - must align "|" with data rows
    # Data format: {5} {5} {6} | {10} {7} | {10} {7} {14} | {10} {7} {14}
    # Section widths after "|": 18 | 33 | 33
    print(f"{'Out':>5} {'In':>5} {'Batch':>6} | "
          f"{'FP16':^18} | "
          f"{'FP8 Cutlass':^33} | "
          f"{'FP8 TRT-LLM':^33}")
    # Second header line with specific metrics - must match exact spacing of data rows
    print(f"{'':>5} {'':>5} {'':>6} | "
          f"{'Median(us)':>10} {'Std(%)':>7} | "
          f"{'Median(us)':>10} {'Std(%)':>7} {'Ratio to FP16':>14} | "
          f"{'Median(us)':>10} {'Std(%)':>7} {'Ratio to FP16':>14}")
    print("-" * 127)
    
    for r in results:
        # FP16 values
        fp16_med = f"{r['fp16_median']:10.2f}" if r['fp16_median'] is not None else "         -"
        fp16_std = f"{r['fp16_std_pct']:7.2f}" if r['fp16_std_pct'] is not None else "      -"
        
        # Cutlass values
        cutlass_med = f"{r['cutlass_median']:10.2f}" if r['cutlass_median'] is not None else "         -"
        cutlass_std = f"{r['cutlass_std_pct']:7.2f}" if r['cutlass_std_pct'] is not None else "      -"
        cutlass_ratio = f"{r['cutlass_ratio']:14.2f}" if r['cutlass_ratio'] is not None else "             -"
        
        # TRT-LLM values
        trtllm_med = f"{r['trtllm_median']:10.2f}" if r['trtllm_median'] is not None else "         -"
        trtllm_std = f"{r['trtllm_std_pct']:7.2f}" if r['trtllm_std_pct'] is not None else "      -"
        trtllm_ratio = f"{r['trtllm_ratio']:14.2f}" if r['trtllm_ratio'] is not None else "             -"
        
        print(f"{r['out']:5d} {r['in']:5d} {r['batch']:6d} | "
              f"{fp16_med} {fp16_std} | "
              f"{cutlass_med} {cutlass_std} {cutlass_ratio} | "
              f"{trtllm_med} {trtllm_std} {trtllm_ratio}")
    
    print("=" * 127)
    print("Notes:")
    print("  - Median: Median execution time in microseconds")
    print("  - Std(%): Standard deviation as percentage of median")
    print("  - Ratio to FP16: FP16 time / FP8 time (higher is better for FP8)")
    print("  - '-' indicates unsupported configuration")
    print("=" * 127)
    
    return results


if __name__ == "__main__":
    results = run_combined_benchmark()

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the FlashInfer implementation in include/flashinfer/gemm/gemm_groupwise_sm100.cuh and the CUTLASS 81_blackwell_gemm_blockwise example, then run the provided B200 benchmark and compare the Nsight Compute metrics. Investigate the ScaleConfig difference and benchmark results across the listed shapes; done means identifying and validating a clear performance improvement for the FP8 CUTLASS path.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python, pytorch
Domain
performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.