Skip to content

Commit c523195

Browse files
committed
Fix fp8 quant FI interface issues (padding + strides)
Signed-off-by: Zhanda <zhandazhu@gmail.com>
1 parent 3641873 commit c523195

3 files changed

Lines changed: 219 additions & 13 deletions

File tree

vllm/model_executor/layers/attention/mm_encoder_attention.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from vllm.config import MultiModalConfig
1212
from vllm.logger import init_logger
1313
from vllm.model_executor.custom_op import CustomOp
14-
from vllm.model_executor.layers.quantization.input_quant_fp8 import QuantFP8
14+
from vllm.model_executor.layers.quantization.input_quant_fp8 import (
15+
QuantFP8,
16+
quantize_fp8_pad_head_dim_triton,
17+
)
1518
from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
1619
from vllm.model_executor.models.vision import get_vit_attn_backend
1720
from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
@@ -174,7 +177,10 @@ def _init_fp8_attention(self, layer_name: str) -> None:
174177
# Create QuantFP8 for efficient quantization
175178
self.fp8_quant = QuantFP8(static=True, group_shape=GroupShape.PER_TENSOR)
176179
self.fp8_enabled = True
177-
180+
self.skip_scale_q = self.fp8_scales["q"] == 1.0
181+
self.skip_scale_k = self.fp8_scales["k"] == 1.0
182+
self.skip_scale_v = self.fp8_scales["v"] == 1.0
183+
178184
logger.debug(
179185
f"FP8 attention enabled for {layer_name}: "
180186
f"q={self.fp8_scales['q']:.4f}, "
@@ -283,17 +289,31 @@ def _quantize_to_fp8(
283289
self,
284290
tensor: torch.Tensor,
285291
scale: torch.Tensor,
292+
skip_scale: bool = False,
286293
) -> torch.Tensor:
287-
"""Quantize a 3D tensor (total_tokens, num_heads, head_dim) to FP8.
288-
289-
Uses QuantFP8 CustomOp for backend-aware quantization.
294+
"""Quantize a 3D (S, H, D) tensor to FP8.
295+
296+
Uses QuantFP8 CustomOp when head_dim is aligned to 16; otherwise
297+
falls back to a stride-aware Triton kernel that pads head_dim to
298+
a multiple of 16 — no extra copy even for non-contiguous inputs.
290299
"""
291300
assert self.fp8_quant is not None
292301
orig_shape = tensor.shape
293-
# QuantFP8 expects 2D input: (total_tokens, num_heads * head_dim)
294-
tensor_2d = tensor.reshape(orig_shape[0], -1)
295-
fp8_tensor, _ = self.fp8_quant.forward_cuda(tensor_2d, scale=scale)
296-
return fp8_tensor.reshape(orig_shape)
302+
head_dim = orig_shape[-1]
303+
304+
if head_dim % 16 == 0:
305+
if skip_scale:
306+
return tensor.to(torch.float8_e4m3fn)
307+
308+
# QuantFP8 expects 2D input: (total_tokens, num_heads * head_dim)
309+
tensor_2d = tensor.reshape(orig_shape[0], -1)
310+
fp8_tensor, _ = self.fp8_quant.forward_cuda(
311+
tensor_2d, scale=scale
312+
)
313+
return fp8_tensor.reshape(orig_shape)
314+
315+
# Fall back to Triton kernel for padding head_dim to a multiple of 16
316+
return quantize_fp8_pad_head_dim_triton(tensor, scale, skip_scale=skip_scale)
297317

298318
def _forward_flashinfer(
299319
self,
@@ -307,11 +327,11 @@ def _forward_flashinfer(
307327
) -> torch.Tensor:
308328
if self.fp8_enabled:
309329
assert self.fp8_quant is not None and self.fp8_scales is not None
310-
query = self._quantize_to_fp8(query, self._fp8_q_scale)
311-
key = self._quantize_to_fp8(key, self._fp8_k_scale)
312-
value = self._quantize_to_fp8(value, self._fp8_v_scale)
330+
query = self._quantize_to_fp8(query, self._fp8_q_scale, skip_scale=self.skip_scale_q)
331+
key = self._quantize_to_fp8(key, self._fp8_k_scale, skip_scale=self.skip_scale_k)
332+
value = self._quantize_to_fp8(value, self._fp8_v_scale, skip_scale=self.skip_scale_v)
313333

314-
return vit_flashinfer_wrapper(
334+
output = vit_flashinfer_wrapper(
315335
q=query,
316336
k=key,
317337
v=value,
@@ -326,6 +346,13 @@ def _forward_flashinfer(
326346
o_data_type=self.dtype if self.fp8_enabled else None,
327347
)
328348

349+
# Un-pad head dimension if it was padded during FP8 quantization
350+
if self.fp8_enabled and output.shape[-1] != self.head_size:
351+
output = output[..., :self.head_size]
352+
output = output.contiguous()
353+
354+
return output
355+
329356
def _forward_fa4(
330357
self,
331358
query: torch.Tensor,

vllm/model_executor/layers/quantization/input_quant_fp8.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33

4+
import os
45
import torch
56
import torch.nn.functional as F
67

@@ -13,12 +14,157 @@
1314
group_broadcast,
1415
)
1516
from vllm.platforms import current_platform
17+
from vllm.triton_utils import HAS_TRITON, tl, triton
1618

1719
_FP8_DTYPE = current_platform.fp8_dtype()
1820
_FP8_MIN, _FP8_MAX = get_fp8_min_max()
1921
_FP8_MIN_SCALING_FACTOR = 1.0 / (_FP8_MAX * 512.0)
2022

2123

24+
@triton.jit
25+
def _quantize_pad_fp8_kernel(
26+
x_ptr,
27+
y_ptr,
28+
scale_ptr,
29+
stride_xs, # input stride along token (seq) dim — may be non-contiguous
30+
stride_xh, # input stride along head dim
31+
stride_xd, # input stride along head_dim dim (usually 1)
32+
stride_ys, # output stride along token dim (contiguous)
33+
stride_yh, # output stride along head dim
34+
stride_yd, # output stride along head_dim dim (usually 1)
35+
num_heads,
36+
n_rows, # total rows = S * H
37+
n_cols,
38+
n_cols_padded,
39+
fp8_min,
40+
fp8_max,
41+
SKIP_SCALE: tl.constexpr,
42+
BLOCK_M: tl.constexpr,
43+
BLOCK_N: tl.constexpr,
44+
):
45+
pid_m = tl.program_id(0)
46+
pid_n = tl.program_id(1)
47+
48+
offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
49+
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
50+
mask_m = offs_m < n_rows
51+
mask_out = mask_m[:, None] & (offs_n[None, :] < n_cols_padded)
52+
mask_in = mask_m[:, None] & (offs_n[None, :] < n_cols)
53+
54+
# Decompose flattened row into (token, head) for 3D stride indexing.
55+
# This lets the kernel read directly from non-contiguous QKV views.
56+
s = offs_m // num_heads
57+
h = offs_m % num_heads
58+
59+
x_ptrs = (x_ptr
60+
+ s[:, None] * stride_xs
61+
+ h[:, None] * stride_xh
62+
+ offs_n[None, :] * stride_xd)
63+
x = tl.load(x_ptrs, mask=mask_in, other=0.0).to(tl.float32)
64+
if SKIP_SCALE:
65+
x_q = x
66+
else:
67+
scale = tl.load(scale_ptr)
68+
x_q = x / scale
69+
x_q = tl.where(mask_in, x_q, 0.0)
70+
x_q = tl.clamp(x_q, fp8_min, fp8_max).to(y_ptr.dtype.element_ty)
71+
72+
y_ptrs = (y_ptr
73+
+ s[:, None] * stride_ys
74+
+ h[:, None] * stride_yh
75+
+ offs_n[None, :] * stride_yd)
76+
tl.store(y_ptrs, x_q, mask=mask_out)
77+
78+
79+
def _get_fp8_pad_quant_config(padded_head_dim: int) -> tuple[int, int, int]:
80+
# Blackwell: use a single static config to avoid recompiles.
81+
if current_platform.is_device_capability_family(100):
82+
block_n, num_warps, block_m = 128, 4, 16
83+
else:
84+
block_n = triton.next_power_of_2(padded_head_dim)
85+
block_n = max(16, min(block_n, 256))
86+
num_warps = 4 if block_n >= 128 else 2
87+
block_m = 16
88+
89+
env_block_n = os.getenv("VLLM_FP8_PAD_QUANT_BLOCK_N")
90+
env_num_warps = os.getenv("VLLM_FP8_PAD_QUANT_NUM_WARPS")
91+
env_block_m = os.getenv("VLLM_FP8_PAD_QUANT_BLOCK_M")
92+
if env_block_n is not None:
93+
block_n = max(16, min(int(env_block_n), 256))
94+
if env_num_warps is not None:
95+
num_warps = int(env_num_warps)
96+
if env_block_m is not None:
97+
block_m = max(1, int(env_block_m))
98+
99+
return block_n, num_warps, block_m
100+
101+
102+
def quantize_fp8_pad_head_dim_triton(
103+
tensor: torch.Tensor,
104+
scale: torch.Tensor,
105+
skip_scale: bool = False,
106+
block_n: int | None = None,
107+
num_warps: int | None = None,
108+
block_m: int | None = None,
109+
) -> torch.Tensor:
110+
"""Quantize a 4D (B, S, H, D) or 3D (S, H, D) tensor to FP8 while padding D to a multiple of 16.
111+
112+
Reads directly from the input using its 3D strides, so non-contiguous
113+
views (e.g. Q/K/V slices from an interleaved QKV buffer) are handled
114+
without an extra copy. Output is always a fresh contiguous tensor
115+
with shape (S, H, padded_D).
116+
"""
117+
if not HAS_TRITON:
118+
raise RuntimeError(
119+
"Triton is required to quantize with head_dim padding."
120+
)
121+
122+
original_shape = tensor.shape
123+
if tensor.dim() == 4:
124+
tensor = tensor.view(-1, tensor.shape[-2], tensor.shape[-1])
125+
assert tensor.dim() == 3, (
126+
f"Expected 3D input (S, H, D), got {tensor.dim()}D"
127+
)
128+
S, H, D = tensor.shape
129+
padded_head_dim = (D + 15) // 16 * 16
130+
out_dtype = current_platform.fp8_dtype()
131+
output = torch.empty(
132+
(S, H, padded_head_dim),
133+
device=tensor.device,
134+
dtype=out_dtype,
135+
)
136+
137+
scale_1d = scale.reshape(-1)
138+
fp8_min, fp8_max = get_fp8_min_max()
139+
n_rows = S * H
140+
141+
if block_n is None or num_warps is None or block_m is None:
142+
block_n, num_warps, block_m = _get_fp8_pad_quant_config(padded_head_dim)
143+
144+
grid = (triton.cdiv(n_rows, block_m),
145+
triton.cdiv(padded_head_dim, block_n))
146+
147+
_quantize_pad_fp8_kernel[grid](
148+
tensor,
149+
output,
150+
scale_1d,
151+
tensor.stride(0), tensor.stride(1), tensor.stride(2),
152+
output.stride(0), output.stride(1), output.stride(2),
153+
H,
154+
n_rows,
155+
D,
156+
padded_head_dim,
157+
fp8_min,
158+
fp8_max,
159+
SKIP_SCALE=skip_scale,
160+
BLOCK_M=block_m,
161+
BLOCK_N=block_n,
162+
num_warps=num_warps,
163+
)
164+
165+
return output.view((*original_shape[:-1], padded_head_dim))
166+
167+
22168
# --8<-- [start:quant_fp8]
23169
@CustomOp.register("quant_fp8")
24170
class QuantFP8(CustomOp):

vllm/model_executor/models/qwen3_vl.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,18 @@ def __init__(
610610

611611
norm_layer = partial(nn.LayerNorm, eps=norm_eps)
612612
head_dim = self.hidden_size // self.num_heads
613+
614+
# When FP8 attention is enabled and head_dim is not a multiple of 16,
615+
# the quantization kernel pads head_dim (e.g. 72 -> 80). Store the
616+
# padded hidden_size so compute_flashinfer_cu_seqlens can produce
617+
# element offsets that match the contiguous FP8 tensor strides.
618+
self.fp8_vit_attn = envs.VLLM_MM_ENCODER_FP8_ATTN
619+
if self.fp8_vit_attn and head_dim % 16 != 0:
620+
padded_head_dim = ((head_dim + 15) // 16) * 16
621+
self.fp8_padded_hidden_size = self.num_heads * padded_head_dim
622+
else:
623+
self.fp8_padded_hidden_size = None
624+
613625
self.rotary_pos_emb = get_rope(
614626
head_size=head_dim,
615627
max_position=8192,
@@ -927,6 +939,27 @@ def compute_flashinfer_cu_seqlens(
927939
rotary_pos_emb_sin: torch.Tensor | None = None,
928940
) -> np.ndarray:
929941
batch_size = len(cu_seqlens) - 1
942+
943+
if self.fp8_padded_hidden_size is not None:
944+
# FP8 path: after quantization Q/K/V are each independent
945+
# contiguous tensors with stride H * padded_D per token.
946+
# All sections (QK, V, O) use the same element stride.
947+
# The wrapper overrides QK/V batch_offsets to use O offsets.
948+
scale = self.fp8_padded_hidden_size // self.tp_size
949+
cu_seqlens = cu_seqlens * scale
950+
cu_seqlens_padded = self.add_padding_to_fi_seqlens(
951+
cu_seqlens, batch_size, cu_seqlens[-1]
952+
)
953+
return np.concatenate(
954+
[cu_seqlens_padded, cu_seqlens_padded, cu_seqlens_padded]
955+
)
956+
957+
# BF16 path: Q/K/V are non-contiguous views into shared buffers.
958+
# Element stride per token differs by tensor:
959+
# After rotary: Q,K in [Q,K] buffer -> stride 2×H×D
960+
# No rotary: Q,K in [Q,K,V] buffer -> stride 3×H×D
961+
# V always in [Q,K,V] buffer -> stride 3×H×D
962+
# O is contiguous -> stride H×D
930963
scale = self.hidden_size // self.tp_size
931964
cu_seqlens = cu_seqlens * scale
932965
if rotary_pos_emb_cos is not None and rotary_pos_emb_sin is not None:

0 commit comments

Comments
 (0)