| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | # Adapted from https://github.com/sgl-project/sglang/pull/2575 |
| 5 | import functools |
| 6 | import json |
| 7 | import os |
| 8 | from collections.abc import Callable, Sequence |
| 9 | from typing import Any |
| 10 | |
| 11 | import torch |
| 12 | |
| 13 | import vllm.envs as envs |
| 14 | from vllm import _custom_ops as ops |
| 15 | from vllm.logger import init_logger |
| 16 | from vllm.model_executor.layers.quantization.utils.quant_utils import ( |
| 17 | amax_for_moe_activation_quant, |
| 18 | get_fp8_min_max, |
| 19 | ) |
| 20 | from vllm.model_executor.layers.quantization.utils.w8a8_utils import ( |
| 21 | all_close_1d, |
| 22 | per_tensor_dequantize, |
| 23 | ) |
| 24 | from vllm.model_executor.parameter import ( |
| 25 | BlockQuantScaleParameter, |
| 26 | ChannelQuantScaleParameter, |
| 27 | PerTensorScaleParameter, |
| 28 | ) |
| 29 | from vllm.model_executor.utils import set_weight_attrs |
| 30 | from vllm.platforms import current_platform |
| 31 | from vllm.triton_utils import tl, triton |
| 32 | from vllm.utils.deep_gemm import ( |
| 33 | get_tma_aligned_size, |
| 34 | is_deep_gemm_e8m0_used, |
| 35 | transform_sf_into_required_layout, |
| 36 | ) |
| 37 | from vllm.utils.platform_utils import get_device_name_as_file_name |
| 38 | |
| 39 | logger = init_logger(__name__) |
| 40 | |
| 41 | # Pre-fill value for scale parameters whose shards load independently. The |
| 42 | # shards are combined with .max(), so an unloaded shard must never win; the |
| 43 | # smallest representable float32 guarantees that. |
| 44 | FP8_SCALE_SENTINEL = torch.finfo(torch.float32).min |
| 45 | |
| 46 | |
| 47 | def is_fp8(x: torch.dtype | torch.Tensor) -> bool: |
| 48 | if isinstance(x, torch.Tensor): |
| 49 | x = x.dtype |
| 50 | return x == torch.float8_e4m3fn or x == torch.float8_e4m3fnuz |
| 51 | |
| 52 | |
| 53 | def get_fp8_block_weight_scale(layer: torch.nn.Module) -> torch.Tensor | None: |
| 54 | """Return the block-FP8 weight scale for supported quant methods.""" |
| 55 | # Local import avoids a circular import: quark_w8a8_fp8 imports this module. |
| 56 | from vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8 import ( |
| 57 | QuarkW8A8Fp8PerBlock, |
| 58 | ) |
| 59 | |
| 60 | if isinstance( |
| 61 | getattr(layer, "scheme", None), |
| 62 | QuarkW8A8Fp8PerBlock, |
| 63 | ): |
| 64 | return getattr(layer, "weight_scale", None) |
| 65 | return getattr(layer, "weight_scale_inv", None) |
| 66 | |
| 67 | |
| 68 | def input_to_float8( |
| 69 | x: torch.Tensor, dtype: torch.dtype | None = None |
| 70 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 71 | """This function quantizes input values to float8 values " |
| 72 | "with tensor-wise quantization.""" |
| 73 | dtype = current_platform.fp8_dtype() if dtype is None else dtype |
| 74 | finfo = torch.finfo(dtype) |
| 75 | min_val, max_val = x.aminmax() |
| 76 | amax = torch.maximum(min_val.abs(), max_val.abs()).clamp(min=1e-12) |
| 77 | scale = finfo.max / amax |
| 78 | x_scl_sat = (x * scale).clamp(min=finfo.min, max=finfo.max) |
| 79 | return x_scl_sat.to(dtype).contiguous(), scale.float().reciprocal() |
| 80 | |
| 81 | |
| 82 | @triton.jit |
| 83 | def _per_token_group_quant_fp8( |
| 84 | # Pointers to inputs and output |
| 85 | y_ptr, |
| 86 | y_q_ptr, |
| 87 | y_s_ptr, |
| 88 | group_size, |
| 89 | # Num columns of y |
| 90 | y_num_columns, |
| 91 | y_row_stride, |
| 92 | # Avoid to divide zero |
| 93 | eps, |
| 94 | # Information for float8 |
| 95 | fp8_min: tl.constexpr, |
| 96 | fp8_max: tl.constexpr, |
| 97 | use_ue8m0: tl.constexpr, |
| 98 | # Meta-parameters |
| 99 | BLOCK: tl.constexpr, |
| 100 | ): |
| 101 | """A Triton-accelerated function to perform per-token-group |
| 102 | quantization on a tensor. |
| 103 | This function converts the tensor values into float8 values. |
| 104 | """ |
| 105 | groups_per_row = y_num_columns // group_size |
| 106 | |
| 107 | # Map the program id to the row of X and Y it should compute. |
| 108 | g_id = tl.program_id(0) |
| 109 | row = g_id // groups_per_row |
| 110 | row_g_id = g_id % groups_per_row |
| 111 | |
| 112 | # Ensure offset calculations use int64 to prevent overflow |
| 113 | y_ptr_offset = (row.to(tl.int64) * y_row_stride) + ( |
| 114 | row_g_id.to(tl.int64) * group_size |
| 115 | ) |
| 116 | y_ptr += y_ptr_offset |
| 117 | |
| 118 | y_q_ptr_offset = g_id.to(tl.int64) * group_size |
| 119 | y_q_ptr += y_q_ptr_offset |
| 120 | y_s_ptr += g_id |
| 121 | |
| 122 | cols = tl.arange(0, BLOCK) # N <= BLOCK |
| 123 | mask = cols < group_size |
| 124 | |
| 125 | y = tl.load(y_ptr + cols, mask=mask, other=0.0).to(tl.float32) |
| 126 | # Quant |
| 127 | # Use multiply-by-reciprocal instead of division to match PyTorch's |
| 128 | # tensor/scalar division precision (GPU fast-division for constexpr |
| 129 | # divisors can introduce 1-ULP error that flips FP8 quantization at |
| 130 | # representable-value boundaries). |
| 131 | _absmax = tl.maximum(tl.max(tl.abs(y)), eps) |
| 132 | scale_raw = _absmax * (1.0 / fp8_max) |
| 133 | y_s = tl.math.exp2(tl.ceil(tl.log2(scale_raw))) if use_ue8m0 else scale_raw |
| 134 | y_q = tl.clamp(y / y_s, fp8_min, fp8_max).to(y_q_ptr.dtype.element_ty) |
| 135 | |
| 136 | tl.store(y_q_ptr + cols, y_q, mask=mask) |
| 137 | tl.store(y_s_ptr, y_s) |
| 138 | |
| 139 | |
| 140 | @triton.jit |
| 141 | def _silu_mul_quant_fp8_packed_kernel( |
| 142 | input_ptr, |
| 143 | output_q_ptr, |
| 144 | output_scale_ptr, |
| 145 | M, |
| 146 | input_stride_m, |
| 147 | output_q_stride_m, |
| 148 | output_scale_stride_k, |
| 149 | clamp_limit, |
| 150 | alpha, |
| 151 | beta, |
| 152 | N: tl.constexpr, |
| 153 | GROUPS_PER_ROW: tl.constexpr, |
| 154 | PACKS_PER_ROW: tl.constexpr, |
| 155 | fp8_min: tl.constexpr, |
| 156 | fp8_max: tl.constexpr, |
| 157 | GROUP_SIZE: tl.constexpr, |
| 158 | PACKS_PER_CTA: tl.constexpr, |
| 159 | BLOCK_M: tl.constexpr, |
| 160 | HAS_CLAMP: tl.constexpr, |
| 161 | ): |
| 162 | GROUPS_PER_PACK: tl.constexpr = 4 |
| 163 | hidden_size: tl.constexpr = N // 2 |
| 164 | |
| 165 | pack_tile = tl.program_id(0) |
| 166 | row_start = tl.program_id(1).to(tl.int64) * BLOCK_M |
| 167 | row_step = tl.num_programs(1).to(tl.int64) * BLOCK_M |
| 168 | |
| 169 | groups_per_cta: tl.constexpr = PACKS_PER_CTA * GROUPS_PER_PACK |
| 170 | elems_per_cta: tl.constexpr = groups_per_cta * GROUP_SIZE |
| 171 | col_start = pack_tile * elems_per_cta |
| 172 | col_offsets = tl.arange(0, elems_per_cta) |
| 173 | row_offsets = tl.arange(0, BLOCK_M) |
| 174 | pack_offsets = tl.arange(0, PACKS_PER_CTA) |
| 175 | |
| 176 | col_mask = (col_start + col_offsets) < (GROUPS_PER_ROW * GROUP_SIZE) |
| 177 | |
| 178 | # persistent with grid_m-stride loop |
| 179 | while row_start < M: |
| 180 | rows = row_start + row_offsets |
| 181 | row_mask = rows < M |
| 182 | input_row_start = rows[:, None] * input_stride_m |
| 183 | output_row_start = rows[:, None] * output_q_stride_m |
| 184 | |
| 185 | gate_flat = tl.load( |
| 186 | input_ptr + input_row_start + col_start + col_offsets[None, :], |
| 187 | mask=row_mask[:, None] & col_mask[None, :], |
| 188 | other=0.0, |
| 189 | ) |
| 190 | up_flat = tl.load( |
| 191 | input_ptr |
| 192 | + input_row_start |
| 193 | + hidden_size |
| 194 | + col_start |
| 195 | + col_offsets[None, :], |
| 196 | mask=row_mask[:, None] & col_mask[None, :], |
| 197 | other=0.0, |
| 198 | ) |
| 199 | |
| 200 | gate = tl.reshape(gate_flat, (BLOCK_M, groups_per_cta, GROUP_SIZE)).to( |
| 201 | tl.float32 |
| 202 | ) |
| 203 | up = tl.reshape(up_flat, (BLOCK_M, groups_per_cta, GROUP_SIZE)).to(tl.float32) |
| 204 | |
| 205 | if HAS_CLAMP: |
| 206 | gate = tl.minimum(gate, clamp_limit) |
| 207 | up = tl.clamp(up, -clamp_limit, clamp_limit) |
| 208 | |
| 209 | # Unified gated activation: silu == swigluoai with alpha=1, beta=0. |
| 210 | # glu = gate * sigmoid(alpha * gate); y = (up + beta) * glu |
| 211 | glu = gate / (1.0 + tl.exp(-gate * alpha)) |
| 212 | y = (up + beta) * glu |
| 213 | # Round through bf16 to match unfused precision path |
| 214 | y = y.to(tl.bfloat16).to(tl.float32) |
| 215 | |
| 216 | absmax = tl.max(tl.abs(y), axis=2) |
| 217 | scale_raw = tl.maximum(absmax / fp8_max, 1e-10) |
| 218 | exponent = tl.ceil(tl.log2(scale_raw)) |
| 219 | scale = tl.math.exp2(exponent) |
| 220 | |
| 221 | y_q = tl.clamp(y / scale[:, :, None], fp8_min, fp8_max) |
| 222 | |
| 223 | y_q_flat = tl.reshape(y_q, (BLOCK_M, elems_per_cta)) |
| 224 | tl.store( |
| 225 | output_q_ptr + output_row_start + col_start + col_offsets[None, :], |
| 226 | y_q_flat.to(output_q_ptr.dtype.element_ty), |
| 227 | mask=row_mask[:, None] & col_mask[None, :], |
| 228 | ) |
| 229 | |
| 230 | scale_byte = tl.clamp(exponent + 127.0, 0.0, 255.0).to(tl.int32) |
| 231 | scale_bytes = tl.reshape(scale_byte, (BLOCK_M, PACKS_PER_CTA, GROUPS_PER_PACK)) |
| 232 | shifts = tl.arange(0, GROUPS_PER_PACK) * 8 |
| 233 | packed_scale = tl.sum(scale_bytes << shifts[None, None, :], axis=2) |
| 234 | |
| 235 | scale_pack = pack_tile * PACKS_PER_CTA + pack_offsets |
| 236 | scale_ptrs = ( |
| 237 | output_scale_ptr |
| 238 | + scale_pack[None, :] * output_scale_stride_k |
| 239 | + rows[:, None] |
| 240 | ) |
| 241 | tl.store( |
| 242 | scale_ptrs, |
| 243 | packed_scale, |
| 244 | mask=row_mask[:, None] & (scale_pack[None, :] < PACKS_PER_ROW), |
| 245 | ) |
| 246 | |
| 247 | row_start += row_step |
| 248 | |
| 249 | |
| 250 | def silu_mul_quant_fp8_packed_triton( |
| 251 | input: torch.Tensor, |
| 252 | group_size: int = 128, |
| 253 | output_q: torch.Tensor | None = None, |
| 254 | clamp_limit: float | None = None, |
| 255 | alpha: float = 1.0, |
| 256 | beta: float = 0.0, |
| 257 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 258 | assert input.dim() == 2 |
| 259 | assert input.is_contiguous() |
| 260 | |
| 261 | M, N = input.shape |
| 262 | hidden_size = N // 2 |
| 263 | |
| 264 | assert hidden_size % group_size == 0 |
| 265 | |
| 266 | fp8_dtype = torch.float8_e4m3fn |
| 267 | finfo = torch.finfo(fp8_dtype) |
| 268 | fp8_min, fp8_max = finfo.min, finfo.max |
| 269 | |
| 270 | groups_per_row = hidden_size // group_size |
| 271 | groups_per_pack = 4 # pack 4 UE8M0 scales to a single INT32 |
| 272 | packs_per_row = triton.cdiv(groups_per_row, groups_per_pack) |
| 273 | |
| 274 | if output_q is None: |
| 275 | output_q = torch.empty((M, hidden_size), dtype=fp8_dtype, device=input.device) |
| 276 | |
| 277 | aligned_m = triton.cdiv(M, 4) * 4 |
| 278 | output_scale_packed = torch.empty( |
| 279 | (packs_per_row, aligned_m), |
| 280 | dtype=torch.int32, |
| 281 | device=input.device, |
| 282 | ).T[:M, :] |
| 283 | |
| 284 | # Tuned for group_size=32 (MXFP8) and group_size=128 (DeepSeek-V4) |
| 285 | num_warps = 4 |
| 286 | num_stages = 2 |
| 287 | if group_size < 128: |
| 288 | BM = 1 |
| 289 | packs_per_cta = 8 |
| 290 | else: |
| 291 | BM = 1 if M < 512 else 4 |
| 292 | packs_per_cta = 2 if M < 512 else 1 |
| 293 | |
| 294 | grid_n = triton.cdiv(packs_per_row, packs_per_cta) |
| 295 | grid_m = min(triton.cdiv(M, BM), 4096) |
| 296 | grid = (grid_n, grid_m) |
| 297 | |
| 298 | has_clamp = clamp_limit is not None |
| 299 | _silu_mul_quant_fp8_packed_kernel[grid]( |
| 300 | input, |
| 301 | output_q, |
| 302 | output_scale_packed, |
| 303 | M, |
| 304 | input.stride(0), |
| 305 | output_q.stride(0), |
| 306 | output_scale_packed.stride(1), |
| 307 | clamp_limit if has_clamp else 0.0, |
| 308 | alpha, |
| 309 | beta, |
| 310 | N=N, |
| 311 | GROUPS_PER_ROW=groups_per_row, |
| 312 | PACKS_PER_ROW=packs_per_row, |
| 313 | fp8_min=fp8_min, |
| 314 | fp8_max=fp8_max, |
| 315 | GROUP_SIZE=group_size, |
| 316 | PACKS_PER_CTA=packs_per_cta, |
| 317 | BLOCK_M=BM, |
| 318 | HAS_CLAMP=has_clamp, |
| 319 | num_warps=num_warps, |
| 320 | num_stages=num_stages, |
| 321 | ) |
| 322 | |
| 323 | return output_q, output_scale_packed |
| 324 | |
| 325 | |
| 326 | @triton.jit |
| 327 | def _silu_mul_per_token_group_quant_fp8_colmajor( |
| 328 | y_ptr, # [M, N] |
| 329 | y_q_ptr, # [M, N // 2] |
| 330 | y_s_ptr, # [M, (N // 2) // GROUP_SIZE] |
| 331 | M, # num tokens |
| 332 | N, # intermediate size |
| 333 | # Stride |
| 334 | y_s_col_stride: tl.int64, |
| 335 | # Information for float8 |
| 336 | eps, |
| 337 | clamp_limit, |
| 338 | alpha, |
| 339 | beta, |
| 340 | fp8_min: tl.constexpr, |
| 341 | fp8_max: tl.constexpr, |
| 342 | use_ue8m0: tl.constexpr, |
| 343 | HAS_CLAMP: tl.constexpr, |
| 344 | # Meta-parameters |
| 345 | GROUP_SIZE: tl.constexpr, |
| 346 | BLOCK_M: tl.constexpr, |
| 347 | BLOCK_N: tl.constexpr, |
| 348 | ): |
| 349 | # TODO(varun) : Add expert_ids so we may early-exit no-op thread blocks. |
| 350 | """Each thread block (BLOCK_N) computes [BLOCK_M, GROUP_SIZE] act-mul outputs. Then |
| 351 | the thread block quantizes the [BLOCK_M, GROUP_SIZE] block of values and fills |
| 352 | the outputs tensors at the right positions. |
| 353 | """ |
| 354 | pid_m = tl.program_id(0) |
| 355 | pid_n = tl.program_id(1) |
| 356 | N_2 = N // 2 |
| 357 | |
| 358 | m_offset = pid_m.to(tl.int64) * BLOCK_M |
| 359 | n_offset = pid_n.to(tl.int64) * BLOCK_N |
| 360 | if m_offset >= M: |
| 361 | return |
| 362 | |
| 363 | offs_n = tl.arange(0, BLOCK_N).to(tl.int64) |
| 364 | offs_m = tl.arange(0, BLOCK_M).to(tl.int64) |
| 365 | |
| 366 | base_y_ptr = y_ptr + m_offset * N + n_offset |
| 367 | |
| 368 | act_in_ptrs = base_y_ptr + offs_m[:, None] * N + offs_n[None, :] |
| 369 | |
| 370 | act_in = tl.load(act_in_ptrs) |
| 371 | mul_in = tl.load(act_in_ptrs + N_2) |
| 372 | |
| 373 | # silu & mul — match C++ silu_and_mul: clamp in fp32 then store back to the |
| 374 | # input dtype, run silu in fp32 then narrow, and do the mul at input |
| 375 | # precision so HAS_CLAMP True/False share the same multiplication path. |
| 376 | if HAS_CLAMP: |
| 377 | act_in = tl.minimum(act_in.to(tl.float32), clamp_limit).to( |
| 378 | y_ptr.dtype.element_ty |
| 379 | ) |
| 380 | mul_in = tl.clamp(mul_in.to(tl.float32), -clamp_limit, clamp_limit).to( |
| 381 | y_ptr.dtype.element_ty |
| 382 | ) |
| 383 | # Unified gated activation: silu == swigluoai with alpha=1, beta=0. |
| 384 | # glu = gate * sigmoid(alpha * gate); y = (up + beta) * glu |
| 385 | # Keep glu/up at input precision (narrow before the mul) so the alpha=1, |
| 386 | # beta=0 defaults match the C++ silu_and_mul path bit-for-bit. |
| 387 | act_in = act_in.to(tl.float32) |
| 388 | glu = (act_in / (1.0 + tl.exp(-act_in * alpha))).to(y_ptr.dtype.element_ty) |
| 389 | up = (mul_in.to(tl.float32) + beta).to(y_ptr.dtype.element_ty) |
| 390 | y = (glu * up).to(tl.float32) |
| 391 | |
| 392 | # quant |
| 393 | _absmax = tl.maximum(tl.max(tl.abs(y), axis=1), eps) |
| 394 | scale_raw = _absmax * (1.0 / fp8_max) |
| 395 | y_s = tl.math.exp2(tl.ceil(tl.log2(scale_raw))) if use_ue8m0 else scale_raw |
| 396 | y_s = tl.reshape(y_s, (BLOCK_M, 1)) |
| 397 | y_q = tl.clamp(y / y_s, fp8_min, fp8_max).to(y_q_ptr.dtype.element_ty) |
| 398 | |
| 399 | # store y_q |
| 400 | base_y_q_ptr = y_q_ptr + m_offset * N_2 + n_offset |
| 401 | y_q_ptrs = base_y_q_ptr + offs_m[:, None] * N_2 + offs_n[None, :] |
| 402 | tl.store(y_q_ptrs, y_q) |
| 403 | |
| 404 | # store y_s |
| 405 | group_id = n_offset // GROUP_SIZE |
| 406 | base_y_s_ptr = y_s_ptr + group_id * y_s_col_stride + m_offset |
| 407 | y_s_ptrs = base_y_s_ptr + offs_m |
| 408 | y_s = tl.reshape(y_s, (BLOCK_M,)) |
| 409 | tl.store(y_s_ptrs, y_s) |
| 410 | |
| 411 | |
| 412 | def silu_mul_per_token_group_quant_fp8_colmajor( |
| 413 | input: torch.Tensor, # [M, N] |
| 414 | output: torch.Tensor | None = None, # [M, N // 2] |
| 415 | use_ue8m0: bool | None = None, |
| 416 | eps: float = 1e-10, |
| 417 | clamp_limit: float | None = None, |
| 418 | group_size: int = 128, |
| 419 | alpha: float = 1.0, |
| 420 | beta: float = 0.0, |
| 421 | ): |
| 422 | """Gated activation + block-fp8 quant. ``alpha``/``beta`` select the gate |
| 423 | (silu: alpha=1, beta=0; swigluoai: alpha, beta from config). |
| 424 | """ |
| 425 | GROUP_SIZE = group_size |
| 426 | assert input.ndim == 2 |
| 427 | if output is not None: |
| 428 | assert output.ndim == 2 |
| 429 | assert input.size(0) % GROUP_SIZE == 0 |
| 430 | assert input.size(1) % (GROUP_SIZE * 2) == 0 |
| 431 | |
| 432 | if use_ue8m0 is None: |
| 433 | use_ue8m0 = is_deep_gemm_e8m0_used() |
| 434 | |
| 435 | M, N = input.size() |
| 436 | N_2 = N // 2 |
| 437 | |
| 438 | fp8_dtype = current_platform.fp8_dtype() |
| 439 | if output is None: |
| 440 | output = torch.empty((M, N_2), dtype=fp8_dtype, device=input.device) |
| 441 | |
| 442 | output_scales = torch.empty( |
| 443 | ((N_2 // GROUP_SIZE), M), dtype=torch.float32, device=input.device |
| 444 | ).transpose(0, 1) |
| 445 | |
| 446 | BLOCK_M = 8 |
| 447 | BLOCK_N = GROUP_SIZE |
| 448 | assert M % BLOCK_M == 0 |
| 449 | assert N_2 % BLOCK_N == 0 |
| 450 | |
| 451 | # Using the default value (240.0) from pytorch will cause accuracy |
| 452 | # issue on dynamic quantization models. Here use 224.0 for fnuz on ROCm |
| 453 | # platforms that use the torch.float8_e4m3fnuz dtype. |
| 454 | finfo = torch.finfo(fp8_dtype) |
| 455 | fp8_min = -224.0 if current_platform.is_fp8_fnuz() else finfo.min |
| 456 | fp8_max = 224.0 if current_platform.is_fp8_fnuz() else finfo.max |
| 457 | |
| 458 | # Force even division so we can avoid edgecases within the kernel. |
| 459 | assert M % BLOCK_M == 0 |
| 460 | assert N_2 % BLOCK_N == 0 |
| 461 | grid = (M // BLOCK_M, N_2 // BLOCK_N) |
| 462 | |
| 463 | has_clamp = clamp_limit is not None |
| 464 | _silu_mul_per_token_group_quant_fp8_colmajor[grid]( |
| 465 | input, |
| 466 | output, |
| 467 | output_scales, |
| 468 | M, |
| 469 | N, |
| 470 | output_scales.stride(-1), |
| 471 | eps, |
| 472 | clamp_limit if has_clamp else 0.0, |
| 473 | alpha, |
| 474 | beta, |
| 475 | fp8_min, |
| 476 | fp8_max, |
| 477 | use_ue8m0, |
| 478 | has_clamp, |
| 479 | GROUP_SIZE, |
| 480 | BLOCK_M, |
| 481 | BLOCK_N, |
| 482 | ) |
| 483 | |
| 484 | return output, output_scales |
| 485 | |
| 486 | |
| 487 | @triton.jit |
| 488 | def _per_token_group_quant_fp8_colmajor( |
| 489 | # Pointers to inputs and output |
| 490 | y_ptr, |
| 491 | y_q_ptr, |
| 492 | y_s_ptr, |
| 493 | group_size, |
| 494 | # Num columns of y |
| 495 | y_num_columns, |
| 496 | y_row_stride, |
| 497 | # Stride from one column to the next of y_s |
| 498 | y_s_col_stride, |
| 499 | # Avoid to divide zero |
| 500 | eps, |
| 501 | # Information for float8 |
| 502 | fp8_min: tl.constexpr, |
| 503 | fp8_max: tl.constexpr, |
| 504 | use_ue8m0: tl.constexpr, |
| 505 | # Meta-parameters |
| 506 | BLOCK: tl.constexpr, |
| 507 | ): |
| 508 | """A Triton-accelerated function to perform per-token-group |
| 509 | quantization on a tensor. |
| 510 | This function converts the tensor values into float8 values. |
| 511 | """ |
| 512 | groups_per_row = y_num_columns // group_size |
| 513 | |
| 514 | # Map the program id to the row of X and Y it should compute. |
| 515 | g_id = tl.program_id(0) |
| 516 | row = g_id // groups_per_row |
| 517 | row_g_id = g_id % groups_per_row |
| 518 | |
| 519 | # Ensure offset calculations use int64 to prevent overflow |
| 520 | y_ptr_offset = (row.to(tl.int64) * y_row_stride) + ( |
| 521 | row_g_id.to(tl.int64) * group_size |
| 522 | ) |
| 523 | y_ptr += y_ptr_offset |
| 524 | |
| 525 | y_q_ptr_offset = g_id.to(tl.int64) * group_size |
| 526 | y_q_ptr += y_q_ptr_offset |
| 527 | |
| 528 | # Convert g_id the flattened block coordinate to 2D so we can index |
| 529 | # into the output y_scales matrix |
| 530 | blocks_per_row = y_num_columns // group_size |
| 531 | scale_col = g_id % blocks_per_row |
| 532 | scale_row = g_id // blocks_per_row |
| 533 | # Ensure offset calculation uses int64 for y_s_ptr |
| 534 | y_s_ptr_offset = (scale_col.to(tl.int64) * y_s_col_stride) + scale_row.to(tl.int64) |
| 535 | y_s_ptr += y_s_ptr_offset |
| 536 | |
| 537 | cols = tl.arange(0, BLOCK) # group_size <= BLOCK |
| 538 | mask = cols < group_size |
| 539 | |
| 540 | y = tl.load(y_ptr + cols, mask=mask, other=0.0).to(tl.float32) |
| 541 | # Quant |
| 542 | _absmax = tl.maximum(tl.max(tl.abs(y)), eps) |
| 543 | scale_raw = _absmax * (1.0 / fp8_max) |
| 544 | y_s = tl.math.exp2(tl.ceil(tl.log2(scale_raw))) if use_ue8m0 else scale_raw |
| 545 | y_q = tl.clamp(y / y_s, fp8_min, fp8_max).to(y_q_ptr.dtype.element_ty) |
| 546 | |
| 547 | tl.store(y_q_ptr + cols, y_q, mask=mask) |
| 548 | tl.store(y_s_ptr, y_s) |
| 549 | |
| 550 | |
| 551 | def per_token_group_quant_fp8( |
| 552 | x: torch.Tensor, |
| 553 | group_size: int, |
| 554 | eps: float = 1e-10, |
| 555 | dtype: torch.dtype | None = None, |
| 556 | column_major_scales: bool = False, |
| 557 | tma_aligned_scales: bool = False, |
| 558 | out_q: torch.Tensor | None = None, |
| 559 | use_ue8m0: bool | None = None, |
| 560 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 561 | """Function to perform per-token-group quantization on an input tensor `x`. |
| 562 | It converts the tensor values into signed float8 values and returns the |
| 563 | quantized tensor along with the scaling factor used for quantization. |
| 564 | |
| 565 | Args: |
| 566 | x: The input tensor with ndim >= 2. |
| 567 | group_size: The group size used for quantization. |
| 568 | eps: The minimum to avoid dividing zero. |
| 569 | dtype: The dtype of output tensor. Note that only `torch.float8_e4m3fn` |
| 570 | is supported for now. |
| 571 | column_major_scales: Outputs scales in column major. |
| 572 | tma_aligned_scales: Outputs scales in TMA-aligned layout. |
| 573 | out_q: Optional output tensor. If not provided, function will create. |
| 574 | use_ue8m0: If True, round each scale down to a power of two (UE8M0 |
| 575 | format). None defers to the platform default. |
| 576 | |
| 577 | Returns: |
| 578 | tuple[torch.Tensor, torch.Tensor]: The quantized tensor and the |
| 579 | scaling factor. |
| 580 | |
| 581 | """ |
| 582 | if use_ue8m0 is None: |
| 583 | use_ue8m0 = is_deep_gemm_e8m0_used() |
| 584 | dtype = current_platform.fp8_dtype() if dtype is None else dtype |
| 585 | assert x.shape[-1] % group_size == 0, ( |
| 586 | f"the last dimension of `x` {x.shape[-1]} must be divisible " |
| 587 | f"by `group_size` {group_size}" |
| 588 | ) |
| 589 | assert x.stride(-1) == 1, "`x` groups must be contiguous" |
| 590 | |
| 591 | fp8_min, fp8_max = get_fp8_min_max() |
| 592 | |
| 593 | assert out_q is None or out_q.shape == x.shape |
| 594 | x_q = out_q |
| 595 | if x_q is None: |
| 596 | x_q = torch.empty(x.shape, device=x.device, dtype=dtype) |
| 597 | |
| 598 | # Allocate the scale tensor in either row- or column-major format. |
| 599 | if column_major_scales: |
| 600 | if tma_aligned_scales: |
| 601 | m = x.shape[-2] |
| 602 | sf_k = x.shape[-1] // group_size |
| 603 | tma_aligned_m = get_tma_aligned_size(m, 4) |
| 604 | shape = x.shape[:-2] + (m, sf_k) |
| 605 | stride = ( |
| 606 | (1, tma_aligned_m) |
| 607 | if x.dim() == 2 |
| 608 | else (tma_aligned_m * sf_k, 1, tma_aligned_m) |
| 609 | ) |
| 610 | x_s = torch.empty_strided( |
| 611 | shape, stride, device=x.device, dtype=torch.float32 |
| 612 | ) |
| 613 | else: |
| 614 | shape = x.shape[:-2] + (x.shape[-1] // group_size, x.shape[-2]) |
| 615 | x_s = torch.empty(shape, device=x.device, dtype=torch.float32).permute( |
| 616 | -1, -2 |
| 617 | ) |
| 618 | else: |
| 619 | shape = x.shape[:-1] + (x.shape[-1] // group_size,) |
| 620 | x_s = torch.empty(shape, device=x.device, dtype=torch.float32) |
| 621 | |
| 622 | # prefer CUDA/XPU kernel if available |
| 623 | # TODO(bnell): this causes some fp8 moe test to fail. |
| 624 | if ( |
| 625 | current_platform.is_cuda_alike() or current_platform.is_xpu() |
| 626 | ) and x.is_contiguous(): |
| 627 | torch.ops._C.per_token_group_fp8_quant( |
| 628 | x, |
| 629 | x_q, |
| 630 | x_s, |
| 631 | group_size, |
| 632 | eps, |
| 633 | fp8_min, |
| 634 | fp8_max, |
| 635 | use_ue8m0, |
| 636 | column_major_scales, |
| 637 | tma_aligned_scales, |
| 638 | ) |
| 639 | return x_q, x_s |
| 640 | |
| 641 | # TRITON FALLBACK |
| 642 | M = x.numel() // group_size |
| 643 | N = group_size |
| 644 | BLOCK = triton.next_power_of_2(N) |
| 645 | # heuristics for number of warps |
| 646 | num_warps = min(max(BLOCK // 256, 1), 8) |
| 647 | num_stages = 1 |
| 648 | if column_major_scales: |
| 649 | _per_token_group_quant_fp8_colmajor[(M,)]( |
| 650 | x, |
| 651 | x_q, |
| 652 | x_s, |
| 653 | group_size, |
| 654 | x.shape[1], |
| 655 | x.stride(0), |
| 656 | x_s.stride(1), |
| 657 | eps, |
| 658 | fp8_min=fp8_min, |
| 659 | fp8_max=fp8_max, |
| 660 | use_ue8m0=use_ue8m0, |
| 661 | BLOCK=BLOCK, |
| 662 | num_warps=num_warps, |
| 663 | num_stages=num_stages, |
| 664 | ) |
| 665 | else: |
| 666 | _per_token_group_quant_fp8[(M,)]( |
| 667 | x, |
| 668 | x_q, |
| 669 | x_s, |
| 670 | group_size, |
| 671 | x.shape[1], |
| 672 | x.stride(0), |
| 673 | eps, |
| 674 | fp8_min=fp8_min, |
| 675 | fp8_max=fp8_max, |
| 676 | use_ue8m0=use_ue8m0, |
| 677 | BLOCK=BLOCK, |
| 678 | num_warps=num_warps, |
| 679 | num_stages=num_stages, |
| 680 | ) |
| 681 | |
| 682 | return x_q, x_s |
| 683 | |
| 684 | |
| 685 | def per_token_group_quant_fp8_packed_for_deepgemm( |
| 686 | x: torch.Tensor, |
| 687 | group_size: int, |
| 688 | eps: float = 1e-10, |
| 689 | use_ue8m0: bool | None = None, |
| 690 | out_q: torch.Tensor | None = None, |
| 691 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 692 | """FP8 per-token-group quantization for DeepGEMM. |
| 693 | |
| 694 | Returns: |
| 695 | (x_q, x_s_packed) |
| 696 | x_q: FP8 activations, same shape as `x`. |
| 697 | x_s_packed: Int32 tensor with logical shape |
| 698 | [mn, ceil(num_groups_per_row / 4)], laid out with |
| 699 | TMA-aligned stride along the packed-K dimension |
| 700 | |
| 701 | """ |
| 702 | if use_ue8m0 is None: |
| 703 | use_ue8m0 = is_deep_gemm_e8m0_used() |
| 704 | # for DeepGEMM UE8M0-packed layout we *require* UE8M0 scales. |
| 705 | assert use_ue8m0, ( |
| 706 | "per_token_group_quant_fp8_packed_for_deepgemm requires UE8M0 scales." |
| 707 | ) |
| 708 | |
| 709 | dtype = current_platform.fp8_dtype() |
| 710 | assert x.shape[-1] % group_size == 0, ( |
| 711 | f"the last dimension of `x` {x.shape[-1]} must be divisible " |
| 712 | f"by `group_size` {group_size}" |
| 713 | ) |
| 714 | assert x.stride(-1) == 1, "`x` groups must be contiguous" |
| 715 | |
| 716 | fp8_min, fp8_max = get_fp8_min_max() |
| 717 | |
| 718 | # compute DeepGEMM-style packed scale tensor shape. |
| 719 | hidden_dim = x.shape[-1] |
| 720 | mn = x.numel() // hidden_dim |
| 721 | num_groups_per_row = hidden_dim // group_size |
| 722 | k_num_packed_sf_k = (num_groups_per_row + 3) // 4 |
| 723 | tma_aligned_mn = ((mn + 3) // 4) * 4 |
| 724 | |
| 725 | x_s_packed = torch.empty_strided( |
| 726 | (mn, k_num_packed_sf_k), |
| 727 | (1, tma_aligned_mn), |
| 728 | device=x.device, |
| 729 | dtype=torch.int32, |
| 730 | ) |
| 731 | |
| 732 | # Native kernel (libtorch stable); used with DeepGEMM on CUDA and |
| 733 | # available on ROCm for the same packed UE8M0 scale layout. |
| 734 | assert current_platform.is_cuda_alike(), ( |
| 735 | "per_token_group_quant_fp8_packed_for_deepgemm requires a CUDA or ROCm GPU." |
| 736 | ) |
| 737 | |
| 738 | x_contiguous = x.contiguous() |
| 739 | if out_q is not None: |
| 740 | x_q_local = out_q |
| 741 | else: |
| 742 | x_q_local = torch.empty_like(x_contiguous, device=x.device, dtype=dtype) |
| 743 | |
| 744 | torch.ops._C.per_token_group_fp8_quant_packed( |
| 745 | x_contiguous, |
| 746 | x_q_local, |
| 747 | x_s_packed, |
| 748 | group_size, |
| 749 | eps, |
| 750 | fp8_min, |
| 751 | fp8_max, |
| 752 | ) |
| 753 | |
| 754 | # return a tensor with the original logical shape. |
| 755 | x_q = x_q_local.view_as(x) |
| 756 | return x_q, x_s_packed |
| 757 | |
| 758 | |
| 759 | @triton.jit |
| 760 | def _w8a8_triton_block_scaled_mm( |
| 761 | # Pointers to inputs and output |
| 762 | A, |
| 763 | B, |
| 764 | C, |
| 765 | As, |
| 766 | Bs, |
| 767 | # Shape for matmul |
| 768 | M, |
| 769 | N, |
| 770 | K, |
| 771 | # Block size for block-wise quantization |
| 772 | group_n, |
| 773 | group_k, |
| 774 | # Stride for inputs and output |
| 775 | stride_am, |
| 776 | stride_ak, |
| 777 | stride_bk, |
| 778 | stride_bn, |
| 779 | stride_cm, |
| 780 | stride_cn, |
| 781 | stride_As_m, |
| 782 | stride_As_k, |
| 783 | stride_Bs_k, |
| 784 | stride_Bs_n, |
| 785 | # Meta-parameters |
| 786 | BLOCK_SIZE_M: tl.constexpr, |
| 787 | BLOCK_SIZE_N: tl.constexpr, |
| 788 | BLOCK_SIZE_K: tl.constexpr, |
| 789 | GROUP_SIZE_M: tl.constexpr, |
| 790 | ): |
| 791 | """Triton-accelerated function used to perform linear operations (dot |
| 792 | product) on input tensors `A` and `B` with block-wise quantization, and |
| 793 | store the result in output tensor `C`. |
| 794 | """ |
| 795 | pid = tl.program_id(axis=0) |
| 796 | num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) |
| 797 | num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) |
| 798 | num_pid_in_group = GROUP_SIZE_M * num_pid_n |
| 799 | group_id = pid // num_pid_in_group |
| 800 | first_pid_m = group_id * GROUP_SIZE_M |
| 801 | group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) |
| 802 | pid_m = first_pid_m + (pid % group_size_m) |
| 803 | pid_n = (pid % num_pid_in_group) // group_size_m |
| 804 | |
| 805 | offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M |
| 806 | offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N |
| 807 | offs_k = tl.arange(0, BLOCK_SIZE_K) |
| 808 | a_ptrs = A + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) |
| 809 | b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) |
| 810 | |
| 811 | As_ptrs = As + offs_am * stride_As_m |
| 812 | offs_bsn = offs_bn // group_n |
| 813 | Bs_ptrs = Bs + offs_bsn * stride_Bs_n |
| 814 | |
| 815 | accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) |
| 816 | for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)): |
| 817 | a = tl.load(a_ptrs, mask=offs_k[None, :] < K - k * BLOCK_SIZE_K, other=0.0) |
| 818 | b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_SIZE_K, other=0.0) |
| 819 | |
| 820 | k_start = k * BLOCK_SIZE_K |
| 821 | offs_ks = k_start // group_k |
| 822 | a_s = tl.load(As_ptrs + offs_ks * stride_As_k) |
| 823 | b_s = tl.load(Bs_ptrs + offs_ks * stride_Bs_k) |
| 824 | |
| 825 | accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :] |
| 826 | a_ptrs += BLOCK_SIZE_K * stride_ak |
| 827 | b_ptrs += BLOCK_SIZE_K * stride_bk |
| 828 | |
| 829 | if C.dtype.element_ty == tl.bfloat16: |
| 830 | c = accumulator.to(tl.bfloat16) |
| 831 | elif C.dtype.element_ty == tl.float16: |
| 832 | c = accumulator.to(tl.float16) |
| 833 | else: |
| 834 | c = accumulator.to(tl.float32) |
| 835 | |
| 836 | offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) |
| 837 | offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) |
| 838 | c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] |
| 839 | c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) |
| 840 | tl.store(c_ptrs, c, mask=c_mask) |
| 841 | |
| 842 | |
| 843 | @functools.lru_cache |
| 844 | def get_w8a8_block_fp8_configs( |
| 845 | N: int, K: int, block_n: int, block_k: int |
| 846 | ) -> dict[int, Any] | None: |
| 847 | """Return optimized configurations for the w8a8 block fp8 kernel. |
| 848 | The return value will be a dictionary that maps an irregular grid of |
| 849 | batch sizes to configurations of the w8a8 block fp8 kernel. To evaluate the |
| 850 | kernel on a given batch size bs, the closest batch size in the grid should |
| 851 | be picked and the associated configuration chosen to invoke the kernel. |
| 852 | """ |
| 853 | # First look up if an optimized configuration is available in the configs |
| 854 | # directory |
| 855 | device_name = get_device_name_as_file_name() |
| 856 | json_file_name = f"N={N},K={K},device_name={device_name},dtype=fp8_w8a8,block_shape=[{block_n},{block_k}].json" # noqa: E501 |
| 857 | |
| 858 | config_file_path = os.path.join( |
| 859 | os.path.dirname(os.path.realpath(__file__)), "configs", json_file_name |
| 860 | ) |
| 861 | if os.path.exists(config_file_path): |
| 862 | with open(config_file_path) as f: |
| 863 | logger.info( |
| 864 | "Using configuration from %s for W8A8 Block FP8 kernel.", |
| 865 | config_file_path, |
| 866 | ) |
| 867 | # If a configuration has been found, return it |
| 868 | return {int(key): val for key, val in json.load(f).items()} |
| 869 | |
| 870 | # If no optimized configuration is available, we will use the default |
| 871 | # configuration |
| 872 | logger.warning( |
| 873 | "Using default W8A8 Block FP8 kernel config. Performance might " |
| 874 | "be sub-optimal! Config file not found at %s", |
| 875 | config_file_path, |
| 876 | ) |
| 877 | return None |
| 878 | |
| 879 | |
| 880 | def w8a8_triton_block_scaled_mm( |
| 881 | A: torch.Tensor, |
| 882 | B: torch.Tensor, |
| 883 | As: torch.Tensor, |
| 884 | Bs: torch.Tensor, |
| 885 | block_size: list[int], |
| 886 | output_dtype: torch.dtype = torch.float16, |
| 887 | ) -> torch.Tensor: |
| 888 | """This function performs matrix multiplication with block-wise |
| 889 | quantization. |
| 890 | It takes two input tensors `A` and `B` with scales `As` and `Bs`. |
| 891 | The output is returned in the specified `output_dtype`. |
| 892 | |
| 893 | Args: |
| 894 | A: The input tensor, e.g., activation. |
| 895 | B: The input tensor, e.g., weight. |
| 896 | As: The per-token-group quantization scale for `A`. |
| 897 | Bs: The per-block quantization scale for `B`. |
| 898 | block_size: The block size for per-block quantization. It should |
| 899 | be 2-dim, e.g., [128, 128]. |
| 900 | output_dtype: The dtype of the returned tensor. |
| 901 | |
| 902 | Returns: |
| 903 | torch.Tensor: The result of matmul. |
| 904 | |
| 905 | """ |
| 906 | _on_gfx1250 = False |
| 907 | if current_platform.is_rocm(): |
| 908 | from vllm.platforms.rocm import on_gfx1250 |
| 909 | |
| 910 | _on_gfx1250 = on_gfx1250() |
| 911 | |
| 912 | if _on_gfx1250: |
| 913 | # Torch upcast reference: dequantize A,B to fp32 and matmul in fp32. |
| 914 | # Avoids the gfx1250 native-fp8 block GEMM NaN bug. Correct but slow. |
| 915 | _bn, _bk = block_size[0], block_size[1] |
| 916 | _As = ( |
| 917 | _upcast_e8m0_to_fp32(As) |
| 918 | if As.dtype == torch.float8_e8m0fnu |
| 919 | else As.to(torch.float32) |
| 920 | ) |
| 921 | _Bs = ( |
| 922 | _upcast_e8m0_to_fp32(Bs) |
| 923 | if Bs.dtype == torch.float8_e8m0fnu |
| 924 | else Bs.to(torch.float32) |
| 925 | ) |
| 926 | _K = A.shape[-1] |
| 927 | _N = B.shape[0] |
| 928 | _Af = A.to(torch.float32).reshape(-1, _K) |
| 929 | _Asf = ( |
| 930 | _As.to(torch.float32) |
| 931 | .reshape(-1, _As.shape[-1]) |
| 932 | .repeat_interleave(_bk, dim=1)[:, :_K] |
| 933 | ) |
| 934 | _Bf = B.to(torch.float32) |
| 935 | _Bsf = _Bs.repeat_interleave(_bn, dim=0).repeat_interleave(_bk, dim=1)[:_N, :_K] |
| 936 | _out = (_Af * _Asf) @ (_Bf * _Bsf).t() |
| 937 | return _out.to(output_dtype).reshape(*A.shape[:-1], _N) |
| 938 | |
| 939 | assert len(block_size) == 2 |
| 940 | block_n, block_k = block_size[0], block_size[1] |
| 941 | |
| 942 | # Triton cannot currently bind E8M0 scale tensors directly. Checkpoints |
| 943 | # with exponent-only UE8M0 scales (e.g. DeepSeek-V4) are decoded to fp32 |
| 944 | # before launching the kernel. |
| 945 | if As.dtype == torch.float8_e8m0fnu: |
| 946 | As = _upcast_e8m0_to_fp32(As).contiguous() |
| 947 | if Bs.dtype == torch.float8_e8m0fnu: |
| 948 | Bs = _upcast_e8m0_to_fp32(Bs).contiguous() |
| 949 | |
| 950 | assert A.shape[-1] == B.shape[-1] |
| 951 | assert A.shape[:-1] == As.shape[:-1] and A.is_contiguous() |
| 952 | assert triton.cdiv(A.shape[-1], block_k) == As.shape[-1] |
| 953 | M = A.numel() // A.shape[-1] |
| 954 | |
| 955 | assert B.ndim == 2 and Bs.ndim == 2 |
| 956 | N, K = B.shape |
| 957 | assert triton.cdiv(N, block_n) == Bs.shape[0] |
| 958 | assert triton.cdiv(K, block_k) == Bs.shape[1] |
| 959 | |
| 960 | C_shape = A.shape[:-1] + (N,) |
| 961 | C = A.new_empty(C_shape, dtype=output_dtype) |
| 962 | |
| 963 | configs = get_w8a8_block_fp8_configs(N, K, block_size[0], block_size[1]) |
| 964 | if configs: |
| 965 | # Get the optimal config if there is one |
| 966 | config = configs[min(configs.keys(), key=lambda x: abs(x - M))] |
| 967 | else: |
| 968 | # Default config |
| 969 | # Block-wise quant: BLOCK_SIZE_N must be divisible by block_size[0] |
| 970 | # BLOCK_SIZE_K must be divisible by block_size[1] |
| 971 | config = { |
| 972 | "BLOCK_SIZE_M": 64, |
| 973 | "BLOCK_SIZE_N": block_size[0], |
| 974 | "BLOCK_SIZE_K": block_size[1], |
| 975 | "GROUP_SIZE_M": 32, |
| 976 | "num_warps": 4, |
| 977 | "num_stages": 2, |
| 978 | } |
| 979 | |
| 980 | def grid(META): |
| 981 | return ( |
| 982 | triton.cdiv(M, META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]), |
| 983 | ) |
| 984 | |
| 985 | _w8a8_triton_block_scaled_mm[grid]( |
| 986 | A, |
| 987 | B, |
| 988 | C, |
| 989 | As, |
| 990 | Bs, |
| 991 | M, |
| 992 | N, |
| 993 | K, |
| 994 | block_n, |
| 995 | block_k, |
| 996 | A.stride(-2), |
| 997 | A.stride(-1), |
| 998 | B.stride(1), |
| 999 | B.stride(0), |
| 1000 | C.stride(-2), |
| 1001 | C.stride(-1), |
| 1002 | As.stride(-2), |
| 1003 | As.stride(-1), |
| 1004 | Bs.stride(1), |
| 1005 | Bs.stride(0), |
| 1006 | **config, |
| 1007 | ) |
| 1008 | |
| 1009 | return C |
| 1010 | |
| 1011 | |
| 1012 | def requant_weight_ue8m0_inplace( |
| 1013 | weight: torch.Tensor, |
| 1014 | weight_scale: torch.Tensor, |
| 1015 | block_size: Sequence[int] = (128, 128), |
| 1016 | ) -> None: |
| 1017 | """Re-quantise *weight* so that its per-block scaling factors are in the |
| 1018 | UE8M0 (power-of-two) format expected by the new DeepGEMM kernels inplace. |
| 1019 | |
| 1020 | Args: |
| 1021 | weight: Block-quantised weight tensor stored in `torch.float8_e4m3fn`. |
| 1022 | Expected shape `(..., M, K)`. |
| 1023 | weight_scale: Corresponding per-block scale tensor (`torch.float32`) |
| 1024 | with shape `(..., M // block_size[0], K // block_size[1])`. |
| 1025 | block_size: 2-element iterable `[block_m, block_k]` describing the |
| 1026 | block quantisation granularity. |
| 1027 | |
| 1028 | """ |
| 1029 | if weight.numel() == 0: |
| 1030 | return |
| 1031 | |
| 1032 | if weight.dtype != torch.float8_e4m3fn: |
| 1033 | raise ValueError( |
| 1034 | f"Expected *weight* to be torch.float8_e4m3fn, got {weight.dtype} instead." |
| 1035 | ) |
| 1036 | |
| 1037 | from vllm.utils.deep_gemm import per_block_cast_to_fp8 |
| 1038 | |
| 1039 | block_m, block_k = int(block_size[0]), int(block_size[1]) |
| 1040 | |
| 1041 | # Flatten leading dimensions so we can iterate over the last two dims. |
| 1042 | leading_shape = weight.shape[:-2] |
| 1043 | if len(leading_shape) == 0: |
| 1044 | w_view = weight.unsqueeze(0) |
| 1045 | s_view = weight_scale.unsqueeze(0) |
| 1046 | else: |
| 1047 | w_view = weight.reshape(-1, weight.shape[-2], weight.shape[-1]) |
| 1048 | s_view = weight_scale.reshape(-1, *weight_scale.shape[-2:]) |
| 1049 | |
| 1050 | num_mats = w_view.size(0) |
| 1051 | for idx in range(num_mats): |
| 1052 | w_q = w_view[idx] |
| 1053 | s_old = s_view[idx] |
| 1054 | |
| 1055 | # De-quantise with the *old* scaling factors (float32). |
| 1056 | m_cur, k_cur = w_q.shape |
| 1057 | s_float = s_old.to(torch.float32) |
| 1058 | # Expand scales along rows and cols by block size, then crop. |
| 1059 | s_exp_r = torch.repeat_interleave(s_float, block_m, dim=0) |
| 1060 | s_exp = torch.repeat_interleave(s_exp_r, block_k, dim=1) |
| 1061 | s_exp = s_exp[:m_cur, :k_cur] |
| 1062 | w_dq = w_q.to(torch.float32) * s_exp |
| 1063 | # Re-quantise using power-of-two scaling (UE8M0). |
| 1064 | w_requant, s_requant = per_block_cast_to_fp8( |
| 1065 | w_dq, [block_m, block_k], use_ue8m0=True |
| 1066 | ) |
| 1067 | |
| 1068 | # Write back the results in-place. |
| 1069 | w_q.copy_(w_requant) |
| 1070 | s_old.copy_(s_requant) |
| 1071 | |
| 1072 | |
| 1073 | def _upcast_e8m0_to_fp32(scale: torch.Tensor) -> torch.Tensor: |
| 1074 | """Upcast E8M0 (exponent-only) scale to float32. |
| 1075 | |
| 1076 | E8M0 stores only the 8-bit biased exponent (bias=127). To convert |
| 1077 | to float32 we place those 8 bits into the exponent field of an |
| 1078 | IEEE-754 float32 (bits 23-30) with sign=0 and mantissa=0. |
| 1079 | """ |
| 1080 | exp_bits = scale.view(torch.uint8).to(torch.int32) |
| 1081 | fp32_bits = exp_bits << 23 |
| 1082 | return fp32_bits.view(torch.float32) |
| 1083 | |
| 1084 | |
| 1085 | def deepgemm_post_process_weight_scale_block( |
| 1086 | ws: torch.Tensor, |
| 1087 | mn: int, |
| 1088 | k: int, |
| 1089 | quant_block_shape: tuple[int, ...], |
| 1090 | num_groups: int, |
| 1091 | is_sfa: bool = False, |
| 1092 | ) -> torch.Tensor: |
| 1093 | if ws.dtype in (torch.float8_e8m0fnu, torch.uint8): |
| 1094 | ws = _upcast_e8m0_to_fp32(ws) |
| 1095 | else: |
| 1096 | assert ws.dtype == torch.float32, ( |
| 1097 | f"Expected tensor scales dtype to be torch.float32 or " |
| 1098 | f"torch.float8_e8m0fnu or torch.uint8, got {ws.dtype} instead" |
| 1099 | ) |
| 1100 | |
| 1101 | return transform_sf_into_required_layout( |
| 1102 | sf=ws, |
| 1103 | mn=mn, |
| 1104 | k=k, |
| 1105 | recipe=(1, quant_block_shape[0], quant_block_shape[1]), |
| 1106 | num_groups=num_groups, |
| 1107 | is_sfa=is_sfa, |
| 1108 | ) |
| 1109 | |
| 1110 | |
| 1111 | def deepgemm_post_process_fp8_weight_block( |
| 1112 | wq: torch.Tensor, |
| 1113 | ws: torch.Tensor, |
| 1114 | quant_block_shape: tuple[int, ...], |
| 1115 | use_e8m0: bool, |
| 1116 | is_bmm: bool = False, |
| 1117 | bmm_batch_size: int = 0, |
| 1118 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 1119 | assert wq.dtype == torch.float8_e4m3fn, ( |
| 1120 | "Expected quantized tensor dtype " |
| 1121 | f"to be torch.float8_e4m3fn, got {wq.dtype} instead." |
| 1122 | ) |
| 1123 | |
| 1124 | if ws.dtype not in (torch.float8_e8m0fnu, torch.uint8): |
| 1125 | assert ws.dtype == torch.float32, ( |
| 1126 | f"Expected tensor scales dtype to be torch.float32 or " |
| 1127 | f"torch.float8_e8m0fnu or torch.uint8, got {ws.dtype} instead" |
| 1128 | ) |
| 1129 | if use_e8m0: |
| 1130 | requant_weight_ue8m0_inplace(wq, ws, block_size=quant_block_shape) |
| 1131 | |
| 1132 | if is_bmm: |
| 1133 | # Reshape 2D weight/scale to 3D for grouped BMM (einsum): |
| 1134 | # wq: (g*r, d) -> (g, r, d) |
| 1135 | # Scale dimensions follow quant_block_shape. |
| 1136 | g = bmm_batch_size |
| 1137 | assert wq.ndim == 2 and ws.ndim == 2 |
| 1138 | d = wq.size(1) |
| 1139 | r = wq.size(0) // g |
| 1140 | wq = wq.view(g, r, d) |
| 1141 | ws = ws.view(g, r // quant_block_shape[0], d // quant_block_shape[1]) |
| 1142 | dg_ws = deepgemm_post_process_weight_scale_block( |
| 1143 | ws=ws, |
| 1144 | mn=r, |
| 1145 | k=d, |
| 1146 | quant_block_shape=quant_block_shape, |
| 1147 | num_groups=g, |
| 1148 | ) |
| 1149 | return wq, dg_ws |
| 1150 | |
| 1151 | original_ndim = wq.ndim |
| 1152 | if wq.ndim == 2: |
| 1153 | assert ws.ndim == 2 |
| 1154 | wq = wq.unsqueeze(0) |
| 1155 | ws = ws.unsqueeze(0) |
| 1156 | |
| 1157 | dg_ws = deepgemm_post_process_weight_scale_block( |
| 1158 | ws=ws, |
| 1159 | mn=wq.size(1), |
| 1160 | k=wq.size(2), |
| 1161 | quant_block_shape=quant_block_shape, |
| 1162 | num_groups=wq.size(0), |
| 1163 | ) |
| 1164 | |
| 1165 | if original_ndim == 2: |
| 1166 | wq = wq.squeeze(0) |
| 1167 | dg_ws = dg_ws.squeeze(0) |
| 1168 | |
| 1169 | return wq, dg_ws |
| 1170 | |
| 1171 | |
| 1172 | def prepare_fp8_moe_layer_for_deepgemm( |
| 1173 | w13: torch.Tensor, |
| 1174 | w2: torch.Tensor, |
| 1175 | w13_scale: torch.Tensor, |
| 1176 | w2_scale: torch.Tensor, |
| 1177 | block_shape: tuple[int], |
| 1178 | ): |
| 1179 | w13, w13_scale = deepgemm_post_process_fp8_weight_block( |
| 1180 | wq=w13, |
| 1181 | ws=w13_scale, |
| 1182 | quant_block_shape=block_shape, |
| 1183 | use_e8m0=is_deep_gemm_e8m0_used(), |
| 1184 | ) |
| 1185 | w2, w2_scale = deepgemm_post_process_fp8_weight_block( |
| 1186 | wq=w2, |
| 1187 | ws=w2_scale, |
| 1188 | quant_block_shape=block_shape, |
| 1189 | use_e8m0=is_deep_gemm_e8m0_used(), |
| 1190 | ) |
| 1191 | |
| 1192 | return w13, w2, w13_scale, w2_scale |
| 1193 | |
| 1194 | |
| 1195 | def _maybe_pad_fp8_weight(weight: torch.Tensor) -> torch.Tensor: |
| 1196 | """Pad the weight tensor. This is an optimization on ROCm platform, which |
| 1197 | can benefit from tensors located far enough from one another in memory""" |
| 1198 | if ( |
| 1199 | envs.VLLM_ROCM_FP8_PADDING |
| 1200 | and current_platform.is_rocm() |
| 1201 | and weight.stride(-1) == 1 |
| 1202 | and (weight.stride(-2) * weight.element_size()) % 512 == 0 |
| 1203 | ): |
| 1204 | num_pad = 256 // weight.element_size() |
| 1205 | import torch.nn.functional as F |
| 1206 | |
| 1207 | weight = F.pad(weight, (0, num_pad), "constant", 0)[..., :-num_pad] |
| 1208 | torch.accelerator.empty_cache() |
| 1209 | return weight |
| 1210 | |
| 1211 | |
| 1212 | def validate_fp8_block_shape( |
| 1213 | layer: torch.nn.Module, |
| 1214 | input_size: int, |
| 1215 | output_size: int, |
| 1216 | input_size_per_partition: int, |
| 1217 | output_partition_sizes: list[int], |
| 1218 | block_size: list[int], |
| 1219 | ) -> None: |
| 1220 | """Validate block quantization shapes for tensor parallelism.""" |
| 1221 | from vllm.distributed import get_tensor_model_parallel_world_size |
| 1222 | |
| 1223 | if getattr(layer, "allow_fp8_block_shape_mismatch", False): |
| 1224 | logger.debug( |
| 1225 | "Skipping FP8 block shape validation for layer %s due to detected" |
| 1226 | " mismatch allowance.", |
| 1227 | getattr(layer, "prefix", "<unknown>"), |
| 1228 | ) |
| 1229 | return |
| 1230 | |
| 1231 | tp_size = getattr(layer, "tp_size", get_tensor_model_parallel_world_size()) |
| 1232 | block_n, block_k = block_size[0], block_size[1] |
| 1233 | |
| 1234 | # Required by row parallel |
| 1235 | if ( |
| 1236 | tp_size > 1 |
| 1237 | and input_size // input_size_per_partition == tp_size |
| 1238 | and input_size_per_partition % block_k != 0 |
| 1239 | ): |
| 1240 | raise ValueError( |
| 1241 | f"Weight input_size_per_partition = {input_size_per_partition} " |
| 1242 | f"is not divisible by weight quantization block_k = {block_k}." |
| 1243 | ) |
| 1244 | |
| 1245 | # Required by column parallel or enabling merged weights |
| 1246 | is_tp_split = tp_size > 1 and output_size // sum(output_partition_sizes) == tp_size |
| 1247 | is_merged_gemm = len(output_partition_sizes) > 1 |
| 1248 | if is_tp_split or is_merged_gemm: |
| 1249 | sizes_to_check = output_partition_sizes |
| 1250 | if not is_tp_split and is_merged_gemm: |
| 1251 | # In case of merged matrices, we allow the last |
| 1252 | # matrix to not be a multiple of block size |
| 1253 | sizes_to_check = output_partition_sizes[:-1] |
| 1254 | for output_partition_size in sizes_to_check: |
| 1255 | if output_partition_size % block_n != 0: |
| 1256 | raise ValueError( |
| 1257 | f"Weight output_partition_size = " |
| 1258 | f"{output_partition_size} is not divisible by " |
| 1259 | f"weight quantization block_n = {block_n}." |
| 1260 | ) |
| 1261 | |
| 1262 | |
| 1263 | def validate_fp8_block_shape_moe( |
| 1264 | intermediate_size_per_partition: int, |
| 1265 | block_size: list[int], |
| 1266 | ) -> None: |
| 1267 | """Validate fused MoE block quantization shapes for tensor parallelism.""" |
| 1268 | from vllm.distributed import get_tensor_model_parallel_world_size |
| 1269 | |
| 1270 | tp_size = get_tensor_model_parallel_world_size() |
| 1271 | block_n, block_k = block_size[0], block_size[1] |
| 1272 | |
| 1273 | # NOTE: To ensure proper alignment of the block-wise quantization |
| 1274 | # scales, the output_size of the weights for both the gate and up |
| 1275 | # layers must be divisible by block_n. |
| 1276 | # Required by column parallel or enabling merged weights |
| 1277 | if intermediate_size_per_partition % block_n != 0: |
| 1278 | raise ValueError( |
| 1279 | f"The output_size of gate's and up's weight = " |
| 1280 | f"{intermediate_size_per_partition} is not divisible by " |
| 1281 | f"weight quantization block_n = {block_n}." |
| 1282 | ) |
| 1283 | if tp_size > 1 and intermediate_size_per_partition % block_k != 0: |
| 1284 | # Required by row parallel |
| 1285 | raise ValueError( |
| 1286 | f"The input_size of down's weight = " |
| 1287 | f"{intermediate_size_per_partition} is not divisible by " |
| 1288 | f"weight quantization block_k = {block_k}." |
| 1289 | ) |
| 1290 | |
| 1291 | |
| 1292 | def create_fp8_weight_parameter( |
| 1293 | output_size_per_partition: int, |
| 1294 | input_size_per_partition: int, |
| 1295 | weight_loader: Callable | None, |
| 1296 | ) -> torch.nn.Parameter: |
| 1297 | """Create FP8 weight parameter.""" |
| 1298 | from vllm.model_executor.parameter import ModelWeightParameter |
| 1299 | |
| 1300 | return ModelWeightParameter( |
| 1301 | data=torch.empty( |
| 1302 | output_size_per_partition, |
| 1303 | input_size_per_partition, |
| 1304 | dtype=torch.float8_e4m3fn, |
| 1305 | ), |
| 1306 | input_dim=1, |
| 1307 | output_dim=0, |
| 1308 | weight_loader=weight_loader, |
| 1309 | ) |
| 1310 | |
| 1311 | |
| 1312 | def create_fp8_scale_parameter( |
| 1313 | parameter_type: torch.nn.Parameter, |
| 1314 | output_partition_sizes: list[int], |
| 1315 | input_size_per_partition: int, |
| 1316 | block_size: list[int] | None, |
| 1317 | weight_loader: Callable | None, |
| 1318 | scale_dtype: torch.dtype | None = None, |
| 1319 | ) -> torch.nn.Parameter: |
| 1320 | """Create scale parameter based on quantization strategy.""" |
| 1321 | dtype = scale_dtype if scale_dtype is not None else torch.float32 |
| 1322 | if parameter_type == ChannelQuantScaleParameter: |
| 1323 | scale = parameter_type( |
| 1324 | data=torch.empty((sum(output_partition_sizes), 1), dtype=dtype), |
| 1325 | output_dim=0, |
| 1326 | weight_loader=weight_loader, |
| 1327 | ) |
| 1328 | elif parameter_type == BlockQuantScaleParameter: |
| 1329 | assert block_size is not None |
| 1330 | block_n, block_k = block_size[0], block_size[1] |
| 1331 | output_size_per_partition = sum(output_partition_sizes) |
| 1332 | scale = parameter_type( |
| 1333 | data=torch.empty( |
| 1334 | (output_size_per_partition + block_n - 1) // block_n, |
| 1335 | (input_size_per_partition + block_k - 1) // block_k, |
| 1336 | dtype=dtype, |
| 1337 | ), |
| 1338 | input_dim=1, |
| 1339 | output_dim=0, |
| 1340 | weight_loader=weight_loader, |
| 1341 | ) |
| 1342 | elif parameter_type == PerTensorScaleParameter: |
| 1343 | scale = parameter_type( |
| 1344 | data=torch.empty(len(output_partition_sizes), dtype=dtype), |
| 1345 | weight_loader=weight_loader, |
| 1346 | ) |
| 1347 | else: |
| 1348 | raise ValueError(f"Unknown parameter type: {parameter_type}") |
| 1349 | |
| 1350 | if dtype == torch.float32: |
| 1351 | scale[:] = torch.finfo(torch.float32).min |
| 1352 | set_weight_attrs(scale, {"scale_type": "weight_scale"}) |
| 1353 | return scale |
| 1354 | |
| 1355 | |
| 1356 | def create_fp8_input_scale( |
| 1357 | output_partition_sizes: list[int], weight_loader: Callable | None |
| 1358 | ) -> torch.nn.Parameter: |
| 1359 | """Create input scale parameter for static activation quantization.""" |
| 1360 | from vllm.model_executor.parameter import PerTensorScaleParameter |
| 1361 | |
| 1362 | scale = PerTensorScaleParameter( |
| 1363 | data=torch.empty(len(output_partition_sizes), dtype=torch.float32), |
| 1364 | weight_loader=weight_loader, |
| 1365 | ) |
| 1366 | scale[:] = torch.finfo(torch.float32).min |
| 1367 | return scale |
| 1368 | |
| 1369 | |
| 1370 | def process_fp8_weight_tensor_strategy( |
| 1371 | weight: torch.Tensor, |
| 1372 | weight_scale: torch.Tensor, |
| 1373 | logical_widths: list[int], |
| 1374 | input_scale: torch.Tensor | None = None, |
| 1375 | ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]: |
| 1376 | """Requantize fused shards to one scale and return ``(K, N)`` weight.""" |
| 1377 | from vllm.model_executor.layers.quantization.utils.w8a8_utils import ( |
| 1378 | normalize_e4m3fn_to_e4m3fnuz, |
| 1379 | requantize_with_max_scale, |
| 1380 | ) |
| 1381 | |
| 1382 | if current_platform.is_fp8_fnuz() and weight.dtype == torch.float8_e4m3fn: |
| 1383 | weight, weight_scale, input_scale = normalize_e4m3fn_to_e4m3fnuz( |
| 1384 | weight=weight, weight_scale=weight_scale, input_scale=input_scale |
| 1385 | ) |
| 1386 | |
| 1387 | # Requantize with max scale |
| 1388 | weight_scale, weight = requantize_with_max_scale( |
| 1389 | weight=weight, |
| 1390 | weight_scale=weight_scale, |
| 1391 | logical_widths=logical_widths, |
| 1392 | ) |
| 1393 | |
| 1394 | weight = _maybe_pad_fp8_weight(weight).t() |
| 1395 | return weight, weight_scale, input_scale |
| 1396 | |
| 1397 | |
| 1398 | def process_fp8_weight_channel_strategy( |
| 1399 | weight: torch.Tensor, |
| 1400 | weight_scale: torch.Tensor, |
| 1401 | input_scale: torch.Tensor | None = None, |
| 1402 | ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]: |
| 1403 | """Normalize FNUZ if needed and return ``(K, N)`` weight.""" |
| 1404 | from vllm.model_executor.layers.quantization.utils.w8a8_utils import ( |
| 1405 | normalize_e4m3fn_to_e4m3fnuz, |
| 1406 | ) |
| 1407 | |
| 1408 | if current_platform.is_fp8_fnuz() and weight.dtype == torch.float8_e4m3fn: |
| 1409 | weight, weight_scale, input_scale = normalize_e4m3fn_to_e4m3fnuz( |
| 1410 | weight=weight, weight_scale=weight_scale, input_scale=input_scale |
| 1411 | ) |
| 1412 | |
| 1413 | return weight.t(), weight_scale, input_scale |
| 1414 | |
| 1415 | |
| 1416 | def process_fp8_weight_block_strategy( |
| 1417 | weight: torch.Tensor, |
| 1418 | weight_scale: torch.Tensor, |
| 1419 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 1420 | """Normalize FNUZ if needed and return ``(N, K)`` weight (no transpose).""" |
| 1421 | from vllm.model_executor.layers.quantization.utils.w8a8_utils import ( |
| 1422 | normalize_e4m3fn_to_e4m3fnuz, |
| 1423 | ) |
| 1424 | |
| 1425 | if current_platform.is_fp8_fnuz() and weight.dtype == torch.float8_e4m3fn: |
| 1426 | if weight_scale.dtype == torch.float8_e8m0fnu: |
| 1427 | # UE8M0 scales: e8m0 stores exponent-only values (2^(exp-127)), |
| 1428 | # so doubling the dequant scale == incrementing the exponent byte |
| 1429 | # by 1. Convert the OCP E4M3 weight bytes to FNUZ in place by |
| 1430 | # reinterpreting and patching the NaN sentinel (-128 in int8), |
| 1431 | # then double the UE8M0 exponent so the dequantized magnitudes |
| 1432 | # match. |
| 1433 | weight_as_int8 = weight.view(torch.int8) |
| 1434 | ROCM_FP8_NAN_AS_INT = -128 |
| 1435 | weight_as_int8[weight_as_int8 == ROCM_FP8_NAN_AS_INT] = 0 |
| 1436 | weight = weight_as_int8.view(torch.float8_e4m3fnuz) |
| 1437 | exp_bytes = weight_scale.view(torch.uint8) |
| 1438 | weight_scale = ( |
| 1439 | (exp_bytes.to(torch.int16) + 1) |
| 1440 | .clamp(max=254) |
| 1441 | .to(torch.uint8) |
| 1442 | .view(torch.float8_e8m0fnu) |
| 1443 | ) |
| 1444 | else: |
| 1445 | weight, weight_scale, _ = normalize_e4m3fn_to_e4m3fnuz( |
| 1446 | weight=weight, weight_scale=weight_scale |
| 1447 | ) |
| 1448 | |
| 1449 | weight = _maybe_pad_fp8_weight(weight) |
| 1450 | return weight, weight_scale |
| 1451 | |
| 1452 | |
| 1453 | def process_fp8_weight_tensor_strategy_moe( |
| 1454 | weight: torch.Tensor, |
| 1455 | weight_scales: torch.Tensor, |
| 1456 | shard_size: int, |
| 1457 | num_experts: int, |
| 1458 | is_act_and_mul: bool = True, |
| 1459 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 1460 | """Process moe weights for tensor-wise quantization strategy.""" |
| 1461 | max_scales = weight_scales.max(dim=1).values |
| 1462 | |
| 1463 | # For w1 case (i.e. not w13): there is already just one scale per expert. |
| 1464 | if not is_act_and_mul: |
| 1465 | assert weight_scales.shape[1] == 1 |
| 1466 | # One scale per expert |
| 1467 | assert max_scales.shape == (num_experts,) |
| 1468 | return weight, max_scales |
| 1469 | |
| 1470 | # For w13 case (common): require single scale for w13 per expert, but |
| 1471 | # on disk there is a scale for w1 and w3. Use the max to requantize. |
| 1472 | for expert_id in range(num_experts): |
| 1473 | start = 0 |
| 1474 | for shard_id in range(2): |
| 1475 | dq_weight = per_tensor_dequantize( |
| 1476 | weight[expert_id][start : start + shard_size, :], |
| 1477 | weight_scales[expert_id][shard_id], |
| 1478 | ) |
| 1479 | weight[expert_id][start : start + shard_size, :], _ = ops.scaled_fp8_quant( |
| 1480 | dq_weight, max_scales[expert_id] |
| 1481 | ) |
| 1482 | start += shard_size |
| 1483 | return weight, max_scales |
| 1484 | |
| 1485 | |
| 1486 | def process_fp8_input_tensor_strategy_moe( |
| 1487 | w13_input_scale: torch.Tensor, |
| 1488 | w2_input_scale: torch.Tensor, |
| 1489 | enable_eplb: bool, |
| 1490 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 1491 | """Process moe input scales for tensor-wise quantization strategy.""" |
| 1492 | if not all_close_1d(w13_input_scale) or not all_close_1d(w2_input_scale): |
| 1493 | logger.info_once( |
| 1494 | "Found input_scales that are not equal for " |
| 1495 | "fp8 MoE layer. Using the maximum across experts " |
| 1496 | "for each layer." |
| 1497 | ) |
| 1498 | |
| 1499 | return ( |
| 1500 | amax_for_moe_activation_quant(w13_input_scale, enable_eplb), |
| 1501 | amax_for_moe_activation_quant(w2_input_scale, enable_eplb), |
| 1502 | ) |