| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | # Adapted from sglang quantization/tuning_block_wise_kernel.py |
| 4 | |
| 5 | import argparse |
| 6 | import json |
| 7 | import multiprocessing as mp |
| 8 | import os |
| 9 | import time |
| 10 | from datetime import datetime |
| 11 | from typing import Any |
| 12 | |
| 13 | import torch |
| 14 | from tqdm import tqdm |
| 15 | |
| 16 | from vllm.model_executor.layers.quantization.utils.fp8_utils import ( |
| 17 | _w8a8_triton_block_scaled_mm, |
| 18 | ) |
| 19 | from vllm.platforms import current_platform |
| 20 | from vllm.triton_utils import triton |
| 21 | from vllm.utils.argparse_utils import FlexibleArgumentParser |
| 22 | from vllm.utils.platform_utils import get_device_name_as_file_name |
| 23 | |
| 24 | mp.set_start_method("spawn", force=True) |
| 25 | |
| 26 | assert ( |
| 27 | current_platform.is_cuda() |
| 28 | or current_platform.is_rocm() |
| 29 | or current_platform.is_xpu() |
| 30 | ), "Only support tune w8a8 block fp8 kernel on CUDA/ROCm/XPU device." |
| 31 | |
| 32 | DTYPE_MAP = { |
| 33 | "float32": torch.float32, |
| 34 | "float16": torch.float16, |
| 35 | "half": torch.half, |
| 36 | "bfloat16": torch.bfloat16, |
| 37 | } |
| 38 | |
| 39 | |
| 40 | def w8a8_block_matmul( |
| 41 | A: torch.Tensor, |
| 42 | B: torch.Tensor, |
| 43 | As: torch.Tensor, |
| 44 | Bs: torch.Tensor, |
| 45 | block_size: list[int], |
| 46 | config: dict[str, Any], |
| 47 | output_dtype: torch.dtype = torch.float16, |
| 48 | ) -> torch.Tensor: |
| 49 | """This function performs matrix multiplication with |
| 50 | block-wise quantization. |
| 51 | |
| 52 | It takes two input tensors `A` and `B` with scales `As` and `Bs`. |
| 53 | The output is returned in the specified `output_dtype`. |
| 54 | |
| 55 | Args: |
| 56 | A: The input tensor, e.g., activation. |
| 57 | B: The input tensor, e.g., weight. |
| 58 | As: The per-token-group quantization scale for `A`. |
| 59 | Bs: The per-block quantization scale for `B`. |
| 60 | block_size: The block size for per-block quantization. |
| 61 | It should be 2-dim, e.g., [128, 128]. |
| 62 | config: Triton kernel launch config, e.g. tile sizes and num_warps. |
| 63 | output_dtype: The dtype of the returned tensor. |
| 64 | |
| 65 | Returns: |
| 66 | torch.Tensor: The result of matmul. |
| 67 | |
| 68 | """ |
| 69 | assert len(block_size) == 2 |
| 70 | block_n, block_k = block_size[0], block_size[1] |
| 71 | |
| 72 | assert A.shape[-1] == B.shape[-1] |
| 73 | assert A.shape[:-1] == As.shape[:-1] and A.is_contiguous() |
| 74 | assert triton.cdiv(A.shape[-1], block_k) == As.shape[-1] |
| 75 | M = A.numel() // A.shape[-1] |
| 76 | |
| 77 | assert B.ndim == 2 and B.is_contiguous() and Bs.ndim == 2 |
| 78 | N, K = B.shape |
| 79 | assert triton.cdiv(N, block_n) == Bs.shape[0] |
| 80 | assert triton.cdiv(K, block_k) == Bs.shape[1] |
| 81 | |
| 82 | C_shape = A.shape[:-1] + (N,) |
| 83 | C = A.new_empty(C_shape, dtype=output_dtype) |
| 84 | |
| 85 | def grid(META): |
| 86 | return ( |
| 87 | triton.cdiv(M, META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]), |
| 88 | ) |
| 89 | |
| 90 | if A.dtype == torch.float8_e4m3fn: |
| 91 | kernel = _w8a8_triton_block_scaled_mm |
| 92 | else: |
| 93 | raise RuntimeError("Currently, only support tune w8a8 block fp8 kernel.") |
| 94 | |
| 95 | kernel[grid]( |
| 96 | A, |
| 97 | B, |
| 98 | C, |
| 99 | As, |
| 100 | Bs, |
| 101 | M, |
| 102 | N, |
| 103 | K, |
| 104 | block_n, |
| 105 | block_k, |
| 106 | A.stride(-2), |
| 107 | A.stride(-1), |
| 108 | B.stride(1), |
| 109 | B.stride(0), |
| 110 | C.stride(-2), |
| 111 | C.stride(-1), |
| 112 | As.stride(-2), |
| 113 | As.stride(-1), |
| 114 | Bs.stride(1), |
| 115 | Bs.stride(0), |
| 116 | **config, |
| 117 | ) |
| 118 | |
| 119 | return C |
| 120 | |
| 121 | |
| 122 | def get_configs_compute_bound(): |
| 123 | configs = [] |
| 124 | for num_stages in [2, 3, 4, 5]: |
| 125 | for block_m in [16, 32, 64, 128, 256]: |
| 126 | for block_k in [64, 128]: |
| 127 | for block_n in [32, 64, 128, 256]: |
| 128 | for num_warps in [4, 8]: |
| 129 | for group_size in [1, 16, 32, 64]: |
| 130 | configs.append( |
| 131 | { |
| 132 | "BLOCK_SIZE_M": block_m, |
| 133 | "BLOCK_SIZE_N": block_n, |
| 134 | "BLOCK_SIZE_K": block_k, |
| 135 | "GROUP_SIZE_M": group_size, |
| 136 | "num_warps": num_warps, |
| 137 | "num_stages": num_stages, |
| 138 | } |
| 139 | ) |
| 140 | return configs |
| 141 | |
| 142 | |
| 143 | def get_xpu_tuning_space(): |
| 144 | # NOTE: curated subset of get_configs_compute_bound(); the full grid is |
| 145 | # too slow to tune on XPU due to per-config Triton JIT compile time. |
| 146 | configs = [] |
| 147 | for num_stages in [2, 3]: |
| 148 | for block_m in [32, 64, 128]: |
| 149 | for block_k in [64, 128]: |
| 150 | for block_n in [128, 256]: |
| 151 | for num_warps in [4, 8, 16, 32]: |
| 152 | for group_size in [1, 32]: |
| 153 | configs.append( |
| 154 | { |
| 155 | "BLOCK_SIZE_M": block_m, |
| 156 | "BLOCK_SIZE_N": block_n, |
| 157 | "BLOCK_SIZE_K": block_k, |
| 158 | "GROUP_SIZE_M": group_size, |
| 159 | "num_warps": num_warps, |
| 160 | "num_stages": num_stages, |
| 161 | } |
| 162 | ) |
| 163 | return configs |
| 164 | |
| 165 | |
| 166 | def get_weight_shapes(tp_size): |
| 167 | # NOTE(HandH1998): The weight shapes only works for DeepSeek-V3. |
| 168 | # Modify them, if you tune for another different model. |
| 169 | # cannot TP |
| 170 | total = [ |
| 171 | (512 + 64, 7168), |
| 172 | (2112, 7168), |
| 173 | ((128 + 64) * 128, 7168), |
| 174 | (128 * (128 + 128), 512), |
| 175 | (7168, 16384), |
| 176 | (7168, 18432), |
| 177 | ] |
| 178 | # N can TP |
| 179 | n_tp = [ |
| 180 | (18432 * 2, 7168), |
| 181 | ((128 + 64) * 128, 7168), |
| 182 | (128 * (128 + 128), 512), |
| 183 | (24576, 1536), |
| 184 | (12288, 7168), |
| 185 | (4096, 7168), |
| 186 | ] |
| 187 | # K can TP |
| 188 | k_tp = [(7168, 18432), (7168, 16384), (7168, 2048)] |
| 189 | |
| 190 | weight_shapes = [] |
| 191 | for t in total: |
| 192 | weight_shapes.append(t) |
| 193 | for n_t in n_tp: |
| 194 | new_t = (n_t[0] // tp_size, n_t[1]) |
| 195 | weight_shapes.append(new_t) |
| 196 | for k_t in k_tp: |
| 197 | new_t = (k_t[0], k_t[1] // tp_size) |
| 198 | weight_shapes.append(new_t) |
| 199 | return weight_shapes |
| 200 | |
| 201 | |
| 202 | def benchmark_config( |
| 203 | A, B, As, Bs, block_size, config, out_dtype=torch.float16, num_iters=10 |
| 204 | ): |
| 205 | def run(): |
| 206 | w8a8_block_matmul(A, B, As, Bs, block_size, config, out_dtype) |
| 207 | |
| 208 | torch.accelerator.synchronize() |
| 209 | # JIT complication & warmup |
| 210 | for _ in range(5): |
| 211 | run() |
| 212 | torch.accelerator.synchronize() |
| 213 | |
| 214 | start_event = torch.Event(enable_timing=True) |
| 215 | end_event = torch.Event(enable_timing=True) |
| 216 | |
| 217 | latencies: list[float] = [] |
| 218 | for i in range(num_iters): |
| 219 | torch.accelerator.synchronize() |
| 220 | start_event.record() |
| 221 | run() |
| 222 | end_event.record() |
| 223 | end_event.synchronize() |
| 224 | latencies.append(start_event.elapsed_time(end_event)) |
| 225 | avg = sum(latencies) / (num_iters * 10) * 1000 # us |
| 226 | return avg |
| 227 | |
| 228 | |
| 229 | def tune(M, N, K, block_size, out_dtype, search_space, input_type): |
| 230 | factor_for_scale = 1e-2 |
| 231 | |
| 232 | if input_type == "fp8": |
| 233 | fp8_info = torch.finfo(torch.float8_e4m3fn) |
| 234 | fp8_max, fp8_min = fp8_info.max, fp8_info.min |
| 235 | |
| 236 | device = current_platform.device_type |
| 237 | A_fp32 = ( |
| 238 | (torch.rand(M, K, dtype=torch.float32, device=device) - 0.5) * 2 * fp8_max |
| 239 | ) |
| 240 | A = A_fp32.clamp(min=fp8_min, max=fp8_max).to(torch.float8_e4m3fn) |
| 241 | |
| 242 | B_fp32 = ( |
| 243 | (torch.rand(N, K, dtype=torch.float32, device=device) - 0.5) * 2 * fp8_max |
| 244 | ) |
| 245 | B = B_fp32.clamp(min=fp8_min, max=fp8_max).to(torch.float8_e4m3fn) |
| 246 | else: |
| 247 | raise RuntimeError("Currently, only support tune w8a8 block fp8 kernel.") |
| 248 | |
| 249 | block_n, block_k = block_size[0], block_size[1] |
| 250 | n_tiles = (N + block_n - 1) // block_n |
| 251 | k_tiles = (K + block_k - 1) // block_k |
| 252 | |
| 253 | As = torch.rand(M, k_tiles, dtype=torch.float32, device=device) * factor_for_scale |
| 254 | Bs = ( |
| 255 | torch.rand(n_tiles, k_tiles, dtype=torch.float32, device=device) |
| 256 | * factor_for_scale |
| 257 | ) |
| 258 | |
| 259 | best_config = None |
| 260 | best_time = float("inf") |
| 261 | for config in tqdm(search_space): |
| 262 | try: |
| 263 | kernel_time = benchmark_config( |
| 264 | A, |
| 265 | B, |
| 266 | As, |
| 267 | Bs, |
| 268 | block_size, |
| 269 | config, |
| 270 | out_dtype, |
| 271 | num_iters=10, |
| 272 | ) |
| 273 | except triton.runtime.autotuner.OutOfResources: |
| 274 | # Some configurations may be invalid and fail to compile. |
| 275 | continue |
| 276 | |
| 277 | if kernel_time < best_time: |
| 278 | best_time = kernel_time |
| 279 | best_config = config |
| 280 | now = datetime.now() |
| 281 | print(f"{now.ctime()}] Completed tuning for batch_size={M}") |
| 282 | assert best_config is not None |
| 283 | return best_config |
| 284 | |
| 285 | |
| 286 | def save_configs( |
| 287 | N, |
| 288 | K, |
| 289 | block_n, |
| 290 | block_k, |
| 291 | configs, |
| 292 | save_path, |
| 293 | input_type="fp8", |
| 294 | ) -> None: |
| 295 | os.makedirs(save_path, exist_ok=True) |
| 296 | device_name = get_device_name_as_file_name() |
| 297 | json_file_name = ( |
| 298 | f"N={N},K={K},device_name={device_name},dtype={input_type}_w8a8," |
| 299 | f"block_shape=[{block_n},{block_k}].json" |
| 300 | ) |
| 301 | |
| 302 | config_file_path = os.path.join(save_path, json_file_name) |
| 303 | print(f"Writing best config to {config_file_path}...") |
| 304 | |
| 305 | with open(config_file_path, "w") as f: |
| 306 | json.dump(configs, f, indent=4) |
| 307 | f.write("\n") |
| 308 | |
| 309 | |
| 310 | def tune_on_gpu(args_dict): |
| 311 | """Run tuning on a specific GPU.""" |
| 312 | gpu_id = args_dict["gpu_id"] |
| 313 | batch_sizes = args_dict["batch_sizes"] |
| 314 | weight_shapes = args_dict["weight_shapes"] |
| 315 | args = args_dict["args"] |
| 316 | |
| 317 | torch.accelerator.set_device_index(gpu_id) |
| 318 | print(f"Starting tuning on GPU {gpu_id} with batch sizes {batch_sizes}") |
| 319 | |
| 320 | block_n = args.block_n |
| 321 | block_k = args.block_k |
| 322 | out_dtype = DTYPE_MAP[args.out_dtype] |
| 323 | save_path = args.save_path |
| 324 | input_type = args.input_type |
| 325 | |
| 326 | search_space = ( |
| 327 | get_xpu_tuning_space() |
| 328 | if current_platform.is_xpu() |
| 329 | else get_configs_compute_bound() |
| 330 | ) |
| 331 | search_space = [ |
| 332 | config for config in search_space if block_k % config["BLOCK_SIZE_K"] == 0 |
| 333 | ] |
| 334 | |
| 335 | start = time.time() |
| 336 | for shape in tqdm(weight_shapes, desc=f"GPU {gpu_id} - Shapes"): |
| 337 | N, K = shape[0], shape[1] |
| 338 | print(f"[GPU {gpu_id}] Tune for weight shape of `N: {N}, K: {K}`") |
| 339 | benchmark_results = [ |
| 340 | tune( |
| 341 | batch_size, |
| 342 | N, |
| 343 | K, |
| 344 | [block_n, block_k], |
| 345 | out_dtype, |
| 346 | search_space, |
| 347 | input_type, |
| 348 | ) |
| 349 | for batch_size in tqdm(batch_sizes, desc=f"GPU {gpu_id} - Batch sizes") |
| 350 | ] |
| 351 | best_configs = {M: config for M, config in zip(batch_sizes, benchmark_results)} |
| 352 | save_configs(N, K, block_n, block_k, best_configs, save_path, input_type) |
| 353 | |
| 354 | end = time.time() |
| 355 | print(f"Tuning on GPU {gpu_id} took {end - start:.2f} seconds") |
| 356 | |
| 357 | |
| 358 | def distribute_batch_sizes(batch_sizes, num_gpus): |
| 359 | """Distribute batch sizes across available GPUs.""" |
| 360 | batches_per_gpu = [] |
| 361 | for i in range(num_gpus): |
| 362 | start_idx = i * len(batch_sizes) // num_gpus |
| 363 | end_idx = (i + 1) * len(batch_sizes) // num_gpus |
| 364 | batches_per_gpu.append(batch_sizes[start_idx:end_idx]) |
| 365 | return batches_per_gpu |
| 366 | |
| 367 | |
| 368 | def main(args): |
| 369 | print(args) |
| 370 | num_gpus = torch.accelerator.device_count() |
| 371 | if num_gpus == 0: |
| 372 | raise RuntimeError("No GPU available for tuning") |
| 373 | print(f"Found {num_gpus} GPUs for parallel tuning") |
| 374 | |
| 375 | if current_platform.is_xpu(): |
| 376 | torch.xpu.init() |
| 377 | else: |
| 378 | torch.cuda.init() |
| 379 | |
| 380 | if args.batch_size is None: |
| 381 | batch_sizes = [ |
| 382 | 1, |
| 383 | 2, |
| 384 | 4, |
| 385 | 8, |
| 386 | 16, |
| 387 | 24, |
| 388 | 32, |
| 389 | 48, |
| 390 | 64, |
| 391 | 96, |
| 392 | 128, |
| 393 | 256, |
| 394 | 512, |
| 395 | 1024, |
| 396 | 1536, |
| 397 | 2048, |
| 398 | 3072, |
| 399 | 4096, |
| 400 | ] |
| 401 | else: |
| 402 | batch_sizes = [args.batch_size] |
| 403 | num_gpus = 1 # If only one batch size, use only one GPU |
| 404 | |
| 405 | weight_shapes = get_weight_shapes(args.tp_size) |
| 406 | |
| 407 | batches_per_gpu = distribute_batch_sizes(batch_sizes, num_gpus) |
| 408 | |
| 409 | process_args = [] |
| 410 | for gpu_id in range(num_gpus): |
| 411 | process_args.append( |
| 412 | { |
| 413 | "gpu_id": gpu_id, |
| 414 | "batch_sizes": batches_per_gpu[gpu_id], |
| 415 | "weight_shapes": weight_shapes, # Each GPU processes all weight shapes |
| 416 | "args": args, |
| 417 | } |
| 418 | ) |
| 419 | |
| 420 | ctx = mp.get_context("spawn") |
| 421 | with ctx.Pool(num_gpus) as pool: |
| 422 | pool.map(tune_on_gpu, process_args) |
| 423 | |
| 424 | print("Multi-GPU tuning completed") |
| 425 | |
| 426 | |
| 427 | if __name__ == "__main__": |
| 428 | parser = FlexibleArgumentParser( |
| 429 | description=""" |
| 430 | Tune triton w8a8 block fp8 for DeepSeek-V3/DeepSeek-R1: |
| 431 | python3 benchmark_w8a8_block_fp8.py --tp-size 8 --input-type fp8 |
| 432 | Then copy to model_executor/layers/quantization/utils/configs |
| 433 | """, |
| 434 | formatter_class=argparse.RawTextHelpFormatter, |
| 435 | ) |
| 436 | |
| 437 | parser.add_argument("--tp-size", "-tp", type=int, default=8) |
| 438 | parser.add_argument("--input-type", type=str, choices=["fp8"], default="fp8") |
| 439 | parser.add_argument( |
| 440 | "--out-dtype", |
| 441 | type=str, |
| 442 | choices=["float32", "float16", "bfloat16", "half"], |
| 443 | default="float16", |
| 444 | ) |
| 445 | parser.add_argument("--block-n", type=int, default=128) |
| 446 | parser.add_argument("--block-k", type=int, default=128) |
| 447 | parser.add_argument("--batch-size", type=int, required=False) |
| 448 | parser.add_argument("--save-path", type=str, default="./") |
| 449 | args = parser.parse_args() |
| 450 | |
| 451 | main(args) |