A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 15, 01:00 UTC
[perf] Add fused MLA QKV + strided layernorm (#21116)
Signed-off-by: Mickael Seznec <mickael@mistral.ai> Co-authored-by: mgoin <mgoin64@gmail.com>
Mickaël Seznec committed
Jul 22, 2025 at 16:07 UTC
4fb56914c5f27ef062e10d44a0f79c6ceab382f9
7 files changed
+214
-66
csrc/layernorm_kernels.cu
+40
-23
@@ -15,15 +15,16 @@ namespace vllm {
15
// TODO(woosuk): Further optimize this kernel.
16
template <typename scalar_t>
17
__global__ void rms_norm_kernel(
18
- scalar_t* __restrict__ out, // [..., hidden_size]
19
- const scalar_t* __restrict__ input, // [..., hidden_size]
18
+ scalar_t* __restrict__ out, // [..., hidden_size]
19
+ const scalar_t* __restrict__ input, // [..., hidden_size]
20
+ const int64_t input_stride,
21
const scalar_t* __restrict__ weight, // [hidden_size]
22
const float epsilon, const int num_tokens, const int hidden_size) {
23
__shared__ float s_variance;
24
float variance = 0.0f;
25
26
for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) {
26
- const float x = (float)input[blockIdx.x * hidden_size + idx];
27
+ const float x = (float)input[blockIdx.x * input_stride + idx];
28
variance += x * x;
29
}
30
@@ -37,7 +38,7 @@ __global__ void rms_norm_kernel(
38
__syncthreads();
39
40
for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) {
40
- float x = (float)input[blockIdx.x * hidden_size + idx];
41
+ float x = (float)input[blockIdx.x * input_stride + idx];
42
out[blockIdx.x * hidden_size + idx] =
43
((scalar_t)(x * s_variance)) * weight[idx];
44
}
@@ -50,7 +51,8 @@ __global__ void rms_norm_kernel(
51
template <typename scalar_t, int width>
52
__global__ std::enable_if_t<(width > 0) && _typeConvert<scalar_t>::exists>
53
fused_add_rms_norm_kernel(
53
- scalar_t* __restrict__ input, // [..., hidden_size]
54
+ scalar_t* __restrict__ input, // [..., hidden_size]
55
+ const int64_t input_stride,
56
scalar_t* __restrict__ residual, // [..., hidden_size]
57
const scalar_t* __restrict__ weight, // [hidden_size]
58
const float epsilon, const int num_tokens, const int hidden_size) {
@@ -59,6 +61,7 @@ fused_add_rms_norm_kernel(
61
static_assert(sizeof(_f16Vec<scalar_t, width>) == sizeof(scalar_t) * width);
62
63
const int vec_hidden_size = hidden_size / width;
64
+ const int64_t vec_input_stride = input_stride / width;
65
__shared__ float s_variance;
66
float variance = 0.0f;
67
/* These and the argument pointers are all declared `restrict` as they are
@@ -73,7 +76,8 @@ fused_add_rms_norm_kernel(
76
77
for (int idx = threadIdx.x; idx < vec_hidden_size; idx += blockDim.x) {
78
int id = blockIdx.x * vec_hidden_size + idx;
76
- _f16Vec<scalar_t, width> temp = input_v[id];
79
+ int64_t strided_id = blockIdx.x * vec_input_stride + idx;
80
+ _f16Vec<scalar_t, width> temp = input_v[strided_id];
81
temp += residual_v[id];
82
variance += temp.sum_squares();
83
residual_v[id] = temp;
@@ -90,10 +94,11 @@ fused_add_rms_norm_kernel(
94
95
for (int idx = threadIdx.x; idx < vec_hidden_size; idx += blockDim.x) {
96
int id = blockIdx.x * vec_hidden_size + idx;
97
+ int64_t strided_id = blockIdx.x * vec_input_stride + idx;
98
_f16Vec<scalar_t, width> temp = residual_v[id];
99
temp *= s_variance;
100
temp *= weight_v[idx];
96
- input_v[id] = temp;
101
+ input_v[strided_id] = temp;
102
}
103
}
104
@@ -103,7 +108,8 @@ fused_add_rms_norm_kernel(
108
template <typename scalar_t, int width>
109
__global__ std::enable_if_t<(width == 0) || !_typeConvert<scalar_t>::exists>
110
fused_add_rms_norm_kernel(
106
- scalar_t* __restrict__ input, // [..., hidden_size]
111
+ scalar_t* __restrict__ input, // [..., hidden_size]
112
+ const int64_t input_stride,
113
scalar_t* __restrict__ residual, // [..., hidden_size]
114
const scalar_t* __restrict__ weight, // [hidden_size]
115
const float epsilon, const int num_tokens, const int hidden_size) {
@@ -111,7 +117,7 @@ fused_add_rms_norm_kernel(
117
float variance = 0.0f;
118
119
for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) {
114
- scalar_t z = input[blockIdx.x * hidden_size + idx];
120
+ scalar_t z = input[blockIdx.x * input_stride + idx];
121
z += residual[blockIdx.x * hidden_size + idx];
122
float x = (float)z;
123
variance += x * x;
@@ -129,7 +135,7 @@ fused_add_rms_norm_kernel(
135
136
for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) {
137
float x = (float)residual[blockIdx.x * hidden_size + idx];
132
- input[blockIdx.x * hidden_size + idx] =
138
+ input[blockIdx.x * input_stride + idx] =
139
((scalar_t)(x * s_variance)) * weight[idx];
140
}
141
}
@@ -141,11 +147,12 @@ void rms_norm(torch::Tensor& out, // [..., hidden_size]
147
torch::Tensor& weight, // [hidden_size]
148
double epsilon) {
149
TORCH_CHECK(out.is_contiguous());
144
- TORCH_CHECK(input.is_contiguous());
150
+ TORCH_CHECK(input.stride(-1) == 1);
151
TORCH_CHECK(weight.is_contiguous());
152
153
int hidden_size = input.size(-1);
154
int num_tokens = input.numel() / hidden_size;
155
+ int64_t input_stride = input.stride(-2);
156
157
dim3 grid(num_tokens);
158
dim3 block(std::min(hidden_size, 1024));
@@ -153,26 +160,29 @@ void rms_norm(torch::Tensor& out, // [..., hidden_size]
160
const cudaStream_t stream = at::cuda::getCurrentCUDAStream();
161
VLLM_DISPATCH_FLOATING_TYPES(input.scalar_type(), "rms_norm_kernel", [&] {
162
vllm::rms_norm_kernel<scalar_t><<<grid, block, 0, stream>>>(
156
- out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(),
163
+ out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), input_stride,
164
weight.data_ptr<scalar_t>(), epsilon, num_tokens, hidden_size);
165
});
166
}
167
161
-#define LAUNCH_FUSED_ADD_RMS_NORM(width) \
162
- VLLM_DISPATCH_FLOATING_TYPES( \
163
- input.scalar_type(), "fused_add_rms_norm_kernel", [&] { \
164
- vllm::fused_add_rms_norm_kernel<scalar_t, width> \
165
- <<<grid, block, 0, stream>>>(input.data_ptr<scalar_t>(), \
166
- residual.data_ptr<scalar_t>(), \
167
- weight.data_ptr<scalar_t>(), epsilon, \
168
- num_tokens, hidden_size); \
168
+#define LAUNCH_FUSED_ADD_RMS_NORM(width) \
169
+ VLLM_DISPATCH_FLOATING_TYPES( \
170
+ input.scalar_type(), "fused_add_rms_norm_kernel", [&] { \
171
+ vllm::fused_add_rms_norm_kernel<scalar_t, width> \
172
+ <<<grid, block, 0, stream>>>( \
173
+ input.data_ptr<scalar_t>(), input_stride, \
174
+ residual.data_ptr<scalar_t>(), weight.data_ptr<scalar_t>(), \
175
+ epsilon, num_tokens, hidden_size); \
176
});
177
178
void fused_add_rms_norm(torch::Tensor& input, // [..., hidden_size]
179
torch::Tensor& residual, // [..., hidden_size]
180
torch::Tensor& weight, // [hidden_size]
181
double epsilon) {
182
+ TORCH_CHECK(residual.is_contiguous());
183
+ TORCH_CHECK(weight.is_contiguous());
184
int hidden_size = input.size(-1);
185
+ int64_t input_stride = input.stride(-2);
186
int num_tokens = input.numel() / hidden_size;
187
188
dim3 grid(num_tokens);
@@ -194,9 +204,16 @@ void fused_add_rms_norm(torch::Tensor& input, // [..., hidden_size]
204
auto inp_ptr = reinterpret_cast<std::uintptr_t>(input.data_ptr());
205
auto res_ptr = reinterpret_cast<std::uintptr_t>(residual.data_ptr());
206
auto wt_ptr = reinterpret_cast<std::uintptr_t>(weight.data_ptr());
197
- bool ptrs_are_aligned =
198
- inp_ptr % 16 == 0 && res_ptr % 16 == 0 && wt_ptr % 16 == 0;
199
- if (ptrs_are_aligned && hidden_size % 8 == 0) {
207
+ constexpr int vector_width = 8;
208
+ constexpr int req_alignment_bytes =
209
+ vector_width * 2; // vector_width * sizeof(bfloat16 or float16) (float32
210
+ // falls back to non-vectorized version anyway)
211
+ bool ptrs_are_aligned = inp_ptr % req_alignment_bytes == 0 &&
212
+ res_ptr % req_alignment_bytes == 0 &&
213
+ wt_ptr % req_alignment_bytes == 0;
214
+ bool offsets_are_multiple_of_vector_width =
215
+ hidden_size % vector_width == 0 && input_stride % vector_width == 0;
216
+ if (ptrs_are_aligned && offsets_are_multiple_of_vector_width) {
217
LAUNCH_FUSED_ADD_RMS_NORM(8);
218
} else {
219
LAUNCH_FUSED_ADD_RMS_NORM(0);
csrc/layernorm_quant_kernels.cu
+25
-14
@@ -23,8 +23,9 @@ namespace vllm {
23
// TODO(woosuk): Further optimize this kernel.
24
template <typename scalar_t, typename fp8_type>
25
__global__ void rms_norm_static_fp8_quant_kernel(
26
- fp8_type* __restrict__ out, // [..., hidden_size]
27
- const scalar_t* __restrict__ input, // [..., hidden_size]
26
+ fp8_type* __restrict__ out, // [..., hidden_size]
27
+ const scalar_t* __restrict__ input, // [..., hidden_size]
28
+ const int input_stride,
29
const scalar_t* __restrict__ weight, // [hidden_size]
30
const float* __restrict__ scale, // [1]
31
const float epsilon, const int num_tokens, const int hidden_size) {
@@ -32,7 +33,7 @@ __global__ void rms_norm_static_fp8_quant_kernel(
33
float variance = 0.0f;
34
35
for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) {
35
- const float x = (float)input[blockIdx.x * hidden_size + idx];
36
+ const float x = (float)input[blockIdx.x * input_stride + idx];
37
variance += x * x;
38
}
39
@@ -49,7 +50,7 @@ __global__ void rms_norm_static_fp8_quant_kernel(
50
float const scale_inv = 1.0f / *scale;
51
52
for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) {
52
- float x = (float)input[blockIdx.x * hidden_size + idx];
53
+ float x = (float)input[blockIdx.x * input_stride + idx];
54
float const out_norm = ((scalar_t)(x * s_variance)) * weight[idx];
55
out[blockIdx.x * hidden_size + idx] =
56
scaled_fp8_conversion<true, fp8_type>(out_norm, scale_inv);
@@ -63,8 +64,9 @@ __global__ void rms_norm_static_fp8_quant_kernel(
64
template <typename scalar_t, int width, typename fp8_type>
65
__global__ std::enable_if_t<(width > 0) && _typeConvert<scalar_t>::exists>
66
fused_add_rms_norm_static_fp8_quant_kernel(
66
- fp8_type* __restrict__ out, // [..., hidden_size]
67
- scalar_t* __restrict__ input, // [..., hidden_size]
67
+ fp8_type* __restrict__ out, // [..., hidden_size]
68
+ scalar_t* __restrict__ input, // [..., hidden_size]
69
+ const int input_stride,
70
scalar_t* __restrict__ residual, // [..., hidden_size]
71
const scalar_t* __restrict__ weight, // [hidden_size]
72
const float* __restrict__ scale, // [1]
@@ -74,6 +76,7 @@ fused_add_rms_norm_static_fp8_quant_kernel(
76
static_assert(sizeof(_f16Vec<scalar_t, width>) == sizeof(scalar_t) * width);
77
78
const int vec_hidden_size = hidden_size / width;
79
+ const int vec_input_stride = input_stride / width;
80
__shared__ float s_variance;
81
float variance = 0.0f;
82
/* These and the argument pointers are all declared `restrict` as they are
@@ -87,8 +90,9 @@ fused_add_rms_norm_static_fp8_quant_kernel(
90
reinterpret_cast<const _f16Vec<scalar_t, width>*>(weight);
91
92
for (int idx = threadIdx.x; idx < vec_hidden_size; idx += blockDim.x) {
93
+ int stride_id = blockIdx.x * vec_input_stride + idx;
94
int id = blockIdx.x * vec_hidden_size + idx;
91
- _f16Vec<scalar_t, width> temp = input_v[id];
95
+ _f16Vec<scalar_t, width> temp = input_v[stride_id];
96
temp += residual_v[id];
97
variance += temp.sum_squares();
98
residual_v[id] = temp;
@@ -125,8 +129,9 @@ fused_add_rms_norm_static_fp8_quant_kernel(
129
template <typename scalar_t, int width, typename fp8_type>
130
__global__ std::enable_if_t<(width == 0) || !_typeConvert<scalar_t>::exists>
131
fused_add_rms_norm_static_fp8_quant_kernel(
128
- fp8_type* __restrict__ out, // [..., hidden_size]
129
- scalar_t* __restrict__ input, // [..., hidden_size]
132
+ fp8_type* __restrict__ out, // [..., hidden_size]
133
+ scalar_t* __restrict__ input, // [..., hidden_size]
134
+ const int input_stride,
135
scalar_t* __restrict__ residual, // [..., hidden_size]
136
const scalar_t* __restrict__ weight, // [hidden_size]
137
const float* __restrict__ scale, // [1]
@@ -135,7 +140,7 @@ fused_add_rms_norm_static_fp8_quant_kernel(
140
float variance = 0.0f;
141
142
for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) {
138
- scalar_t z = input[blockIdx.x * hidden_size + idx];
143
+ scalar_t z = input[blockIdx.x * input_stride + idx];
144
z += residual[blockIdx.x * hidden_size + idx];
145
float x = (float)z;
146
variance += x * x;
@@ -169,7 +174,9 @@ void rms_norm_static_fp8_quant(torch::Tensor& out, // [..., hidden_size]
174
torch::Tensor& weight, // [hidden_size]
175
torch::Tensor& scale, // [1]
176
double epsilon) {
177
+ TORCH_CHECK(out.is_contiguous());
178
int hidden_size = input.size(-1);
179
+ int input_stride = input.stride(-2);
180
int num_tokens = input.numel() / hidden_size;
181
182
dim3 grid(num_tokens);
@@ -183,8 +190,9 @@ void rms_norm_static_fp8_quant(torch::Tensor& out, // [..., hidden_size]
190
vllm::rms_norm_static_fp8_quant_kernel<scalar_t, fp8_t>
191
<<<grid, block, 0, stream>>>(
192
out.data_ptr<fp8_t>(), input.data_ptr<scalar_t>(),
186
- weight.data_ptr<scalar_t>(), scale.data_ptr<float>(),
187
- epsilon, num_tokens, hidden_size);
193
+ input_stride, weight.data_ptr<scalar_t>(),
194
+ scale.data_ptr<float>(), epsilon, num_tokens,
195
+ hidden_size);
196
});
197
});
198
}
@@ -198,7 +206,7 @@ void rms_norm_static_fp8_quant(torch::Tensor& out, // [..., hidden_size]
206
width, fp8_t> \
207
<<<grid, block, 0, stream>>>( \
208
out.data_ptr<fp8_t>(), input.data_ptr<scalar_t>(), \
201
- residual.data_ptr<scalar_t>(), \
209
+ input_stride, residual.data_ptr<scalar_t>(), \
210
weight.data_ptr<scalar_t>(), scale.data_ptr<float>(), \
211
epsilon, num_tokens, hidden_size); \
212
}); \
@@ -210,7 +218,10 @@ void fused_add_rms_norm_static_fp8_quant(
218
torch::Tensor& weight, // [hidden_size]
219
torch::Tensor& scale, // [1]
220
double epsilon) {
221
+ TORCH_CHECK(out.is_contiguous());
222
+ TORCH_CHECK(residual.is_contiguous());
223
int hidden_size = input.size(-1);
224
+ int input_stride = input.stride(-2);
225
int num_tokens = input.numel() / hidden_size;
226
227
dim3 grid(num_tokens);
@@ -234,7 +245,7 @@ void fused_add_rms_norm_static_fp8_quant(
245
auto wt_ptr = reinterpret_cast<std::uintptr_t>(weight.data_ptr());
246
bool ptrs_are_aligned =
247
inp_ptr % 16 == 0 && res_ptr % 16 == 0 && wt_ptr % 16 == 0;
237
- if (ptrs_are_aligned && hidden_size % 8 == 0) {
248
+ if (ptrs_are_aligned && hidden_size % 8 == 0 && input_stride % 8 == 0) {
249
LAUNCH_FUSED_ADD_RMS_NORM(8);
250
} else {
251
LAUNCH_FUSED_ADD_RMS_NORM(0);
csrc/quantization/fp8/common.cu
+4
@@ -88,6 +88,8 @@ void static_scaled_fp8_quant(torch::Tensor& out, // [..., d]
88
torch::Tensor const& input, // [..., d]
89
torch::Tensor const& scale) // [1]
90
{
91
+ TORCH_CHECK(input.is_contiguous());
92
+ TORCH_CHECK(out.is_contiguous());
93
int const block_size = 256;
94
int const num_tokens = input.numel() / input.size(-1);
95
int const num_elems = input.numel();
@@ -111,6 +113,8 @@ void dynamic_scaled_fp8_quant(torch::Tensor& out, // [..., d]
113
torch::Tensor const& input, // [..., d]
114
torch::Tensor& scale) // [1]
115
{
116
+ TORCH_CHECK(input.is_contiguous());
117
+ TORCH_CHECK(out.is_contiguous());
118
int const block_size = 256;
119
int const num_tokens = input.numel() / input.size(-1);
120
int const num_elems = input.numel();
tests/kernels/core/test_layernorm.py
+19
-7
@@ -26,6 +26,7 @@ CUDA_DEVICES = [
26
@pytest.mark.parametrize("dtype", DTYPES)
27
@pytest.mark.parametrize("seed", SEEDS)
28
@pytest.mark.parametrize("device", CUDA_DEVICES)
29
+@pytest.mark.parametrize("strided_input", [False, True])
30
@torch.inference_mode()
31
def test_rms_norm(
32
num_tokens: int,
@@ -34,13 +35,17 @@ def test_rms_norm(
35
dtype: torch.dtype,
36
seed: int,
37
device: str,
38
+ strided_input: bool,
39
) -> None:
40
current_platform.seed_everything(seed)
41
torch.set_default_device(device)
42
layer = RMSNorm(hidden_size).to(dtype=dtype)
43
layer.weight.data.normal_(mean=1.0, std=0.1)
44
scale = 1 / (2 * hidden_size)
43
- x = torch.randn(num_tokens, hidden_size, dtype=dtype)
45
+ last_dim = 2 * hidden_size if strided_input else hidden_size
46
+ x = torch.randn(num_tokens, last_dim, dtype=dtype)
47
+ x = x[..., :hidden_size]
48
+ assert x.is_contiguous() != strided_input
49
x *= scale
50
residual = torch.randn_like(x) * scale if add_residual else None
51
@@ -72,6 +77,7 @@ def test_rms_norm(
77
@pytest.mark.parametrize("quant_scale", [1.0, 0.01, 10.0])
78
@pytest.mark.parametrize("seed", SEEDS)
79
@pytest.mark.parametrize("device", CUDA_DEVICES)
80
+@pytest.mark.parametrize("strided_input", [False, True])
81
def test_fused_rms_norm_quant(
82
num_tokens: int,
83
hidden_size: int,
@@ -80,13 +86,18 @@ def test_fused_rms_norm_quant(
86
quant_scale: float,
87
seed: int,
88
device: str,
89
+ strided_input: bool,
90
) -> None:
91
current_platform.seed_everything(seed)
92
torch.set_default_device(device)
93
94
weight = torch.empty(hidden_size, dtype=dtype).normal_(mean=1.0, std=0.1)
95
scale = 1 / (2 * hidden_size)
89
- x = torch.randn(num_tokens, hidden_size, dtype=dtype)
96
+ last_dim = 2 * hidden_size if strided_input else hidden_size
97
+ x_base = torch.randn(num_tokens, last_dim, dtype=dtype)
98
+ x = x_base[..., :hidden_size]
99
+ assert x.is_contiguous() != strided_input
100
+
101
x *= scale
102
if add_residual:
103
residual = torch.randn_like(x) * scale
@@ -106,9 +117,11 @@ def test_fused_rms_norm_quant(
117
118
# Unfused kernel is in-place so it goes second
119
# Also use a separate clone of x to avoid modifying the input
109
- x_unfused = x.clone()
120
+ x_unfused_base = x_base.clone()
121
+ x_unfused = x_unfused_base[..., :hidden_size]
122
+ assert x_unfused.is_contiguous() != strided_input
123
torch.ops._C.fused_add_rms_norm(x_unfused, residual, weight, 1e-6)
111
- torch.ops._C.static_scaled_fp8_quant(out_quant, x_unfused,
124
+ torch.ops._C.static_scaled_fp8_quant(out_quant, x_unfused.contiguous(),
125
quant_scale_t)
126
127
torch.cuda.synchronize()
@@ -116,7 +129,6 @@ def test_fused_rms_norm_quant(
129
residual,
130
atol=1e-2,
131
rtol=1e-2)
119
-
132
opcheck(
133
torch.ops._C.fused_add_rms_norm_static_fp8_quant,
134
(out_quant_fused, x, residual_fused, weight, quant_scale_t, 1e-6))
@@ -131,7 +143,7 @@ def test_fused_rms_norm_quant(
143
opcheck(torch.ops._C.rms_norm_static_fp8_quant,
144
(out_quant_fused, x, weight, quant_scale_t, 1e-6))
145
134
- torch.testing.assert_close(out_quant_fused.to(dtype=torch.float32),
135
- out_quant.to(dtype=torch.float32),
146
+ torch.testing.assert_close(out_quant.to(dtype=torch.float32),
147
+ out_quant_fused.to(dtype=torch.float32),
148
atol=1e-3,
149
rtol=1e-3)
vllm/model_executor/layers/linear.py
+77
-1
@@ -259,6 +259,8 @@ class LinearBase(torch.nn.Module):
259
if params_dtype is None:
260
params_dtype = torch.get_default_dtype()
261
self.params_dtype = params_dtype
262
+ self.quant_config = quant_config
263
+ self.prefix = prefix
264
if quant_config is None:
265
self.quant_method: Optional[
266
QuantizeMethodBase] = UnquantizedLinearMethod()
@@ -300,6 +302,12 @@ class ReplicatedLinear(LinearBase):
302
*,
303
return_bias: bool = True,
304
):
305
+ # If MergedReplicatedLinear, use output size of each partition.
306
+ if hasattr(self, "output_sizes"):
307
+ self.output_partition_sizes = self.output_sizes
308
+ else:
309
+ self.output_partition_sizes = [output_size]
310
+
311
super().__init__(input_size,
312
output_size,
313
skip_bias_add,
@@ -311,7 +319,8 @@ class ReplicatedLinear(LinearBase):
319
# All the linear layer supports quant method.
320
assert self.quant_method is not None
321
self.quant_method.create_weights(self,
314
- self.input_size, [self.output_size],
322
+ self.input_size,
323
+ self.output_partition_sizes,
324
self.input_size,
325
self.output_size,
326
self.params_dtype,
@@ -367,6 +376,73 @@ class ReplicatedLinear(LinearBase):
376
return s
377
378
379
+class MergedReplicatedLinear(ReplicatedLinear):
380
+ """Replicated linear layer.
381
+
382
+ Args:
383
+ input_size: input dimension of the linear layer.
384
+ output_size: output dimension of the linear layer.
385
+ bias: If true, add bias.
386
+ skip_bias_add: If true, skip adding bias but instead return it.
387
+ params_dtype: Data type for the parameters.
388
+ quant_config: Quantization configure.
389
+ prefix: The name of the layer in the state dict, including all parents
390
+ (e.g. model.layers.0.qkv_proj)
391
+ """
392
+
393
+ def __init__(
394
+ self,
395
+ input_size: int,
396
+ output_sizes: list[int],
397
+ bias: bool = True,
398
+ skip_bias_add: bool = False,
399
+ params_dtype: Optional[torch.dtype] = None,
400
+ quant_config: Optional[QuantizationConfig] = None,
401
+ prefix: str = "",
402
+ *,
403
+ return_bias: bool = True,
404
+ ):
405
+ self.output_sizes = output_sizes
406
+ super().__init__(input_size,
407
+ sum(output_sizes),
408
+ bias,
409
+ skip_bias_add,
410
+ params_dtype,
411
+ quant_config,
412
+ prefix=prefix,
413
+ return_bias=return_bias)
414
+
415
+ def weight_loader(self,
416
+ param: Union[Parameter, BasevLLMParameter],
417
+ loaded_weight: torch.Tensor,
418
+ loaded_shard_id: Optional[int] = None):
419
+ assert loaded_shard_id is not None
420
+ assert loaded_shard_id < len(self.output_sizes)
421
+
422
+ if isinstance(param, BlockQuantScaleParameter):
423
+ from vllm.model_executor.layers.quantization.fp8 import (
424
+ Fp8LinearMethod, Fp8MoEMethod)
425
+ assert self.quant_method is not None
426
+ assert isinstance(self.quant_method,
427
+ (Fp8LinearMethod, Fp8MoEMethod))
428
+ weight_block_size = self.quant_method.quant_config.weight_block_size
429
+ assert weight_block_size is not None
430
+ block_n, _ = weight_block_size[0], weight_block_size[1]
431
+ shard_offset = (
432
+ (sum(self.output_sizes[:loaded_shard_id]) + block_n - 1) //
433
+ block_n)
434
+ shard_size = ((self.output_sizes[loaded_shard_id] + block_n - 1) //
435
+ block_n)
436
+ elif isinstance(param, PerTensorScaleParameter):
437
+ shard_offset = loaded_shard_id
438
+ shard_size = 1
439
+ else:
440
+ shard_offset = sum(self.output_sizes[:loaded_shard_id])
441
+ shard_size = self.output_sizes[loaded_shard_id]
442
+
443
+ param[shard_offset:shard_offset + shard_size] = loaded_weight
444
+
445
+
446
class ColumnParallelLinear(LinearBase):
447
"""Linear layer with column parallelism.
448
vllm/model_executor/layers/quantization/fp8.py
+10
-3
@@ -257,9 +257,16 @@ class Fp8LinearMethod(LinearMethodBase):
257
f"{input_size_per_partition} is not divisible by "
258
f"weight quantization block_k = {block_k}.")
259
# Required by column parallel or enabling merged weights
260
- if (tp_size > 1 and output_size // output_size_per_partition
261
- == tp_size) or len(output_partition_sizes) > 1:
262
- for output_partition_size in output_partition_sizes:
260
+ is_tp_split = (tp_size > 1 and
261
+ output_size // output_size_per_partition == tp_size)
262
+ is_merged_gemm = len(output_partition_sizes) > 1
263
+ if is_tp_split or is_merged_gemm:
264
+ sizes_to_check = output_partition_sizes
265
+ if not is_tp_split and is_merged_gemm:
266
+ # In case of merged matrices, we allow the last
267
+ # matrix to not be a multiple of block size
268
+ sizes_to_check = output_partition_sizes[:-1]
269
+ for output_partition_size in sizes_to_check:
270
if output_partition_size % block_n != 0:
271
raise ValueError(
272
f"Weight output_partition_size = "
vllm/model_executor/models/deepseek_v2.py
+39
-18
@@ -42,6 +42,7 @@ from vllm.model_executor.layers.fused_moe import FusedMoE
42
from vllm.model_executor.layers.layernorm import RMSNorm
43
from vllm.model_executor.layers.linear import (ColumnParallelLinear,
44
MergedColumnParallelLinear,
45
+ MergedReplicatedLinear,
46
ReplicatedLinear,
47
RowParallelLinear)
48
from vllm.model_executor.layers.logits_processor import LogitsProcessor
@@ -336,7 +337,7 @@ class DeepseekV2Attention(nn.Module):
337
kv_a, _ = latent_cache.split(
338
[self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
339
latent_cache = latent_cache.unsqueeze(1)
339
- kv_a = self.kv_a_layernorm(kv_a.contiguous())
340
+ kv_a = self.kv_a_layernorm(kv_a)
341
kv = self.kv_b_proj(kv_a)[0]
342
kv = kv.view(-1, self.num_local_heads,
343
self.qk_nope_head_dim + self.v_head_dim)
@@ -407,14 +408,24 @@ class DeepseekV2MLAAttention(nn.Module):
408
self.max_position_embeddings = max_position_embeddings
409
410
if self.q_lora_rank is not None:
410
- self.q_a_proj = ReplicatedLinear(self.hidden_size,
411
- self.q_lora_rank,
412
- bias=False,
413
- quant_config=quant_config,
414
- prefix=f"{prefix}.q_a_proj")
411
+ self.fused_qkv_a_proj = MergedReplicatedLinear(
412
+ self.hidden_size,
413
+ [self.q_lora_rank, self.kv_lora_rank + self.qk_rope_head_dim],
414
+ bias=False,
415
+ quant_config=quant_config,
416
+ prefix=f"{prefix}.fused_qkv_a_proj")
417
+ else:
418
+ self.kv_a_proj_with_mqa = ReplicatedLinear(
419
+ self.hidden_size,
420
+ self.kv_lora_rank + self.qk_rope_head_dim,
421
+ bias=False,
422
+ quant_config=quant_config,
423
+ prefix=f"{prefix}.kv_a_proj_with_mqa")
424
+
425
+ if self.q_lora_rank is not None:
426
self.q_a_layernorm = RMSNorm(self.q_lora_rank,
427
eps=config.rms_norm_eps)
417
- self.q_b_proj = ColumnParallelLinear(q_lora_rank,
428
+ self.q_b_proj = ColumnParallelLinear(self.q_lora_rank,
429
self.num_heads *
430
self.qk_head_dim,
431
bias=False,
@@ -427,13 +438,6 @@ class DeepseekV2MLAAttention(nn.Module):
438
bias=False,
439
quant_config=quant_config,
440
prefix=f"{prefix}.q_proj")
430
-
431
- self.kv_a_proj_with_mqa = ReplicatedLinear(
432
- self.hidden_size,
433
- self.kv_lora_rank + self.qk_rope_head_dim,
434
- bias=False,
435
- quant_config=quant_config,
436
- prefix=f"{prefix}.kv_a_proj_with_mqa")
441
self.kv_a_layernorm = RMSNorm(self.kv_lora_rank,
442
eps=config.rms_norm_eps)
443
self.kv_b_proj = ColumnParallelLinear(
@@ -495,15 +499,24 @@ class DeepseekV2MLAAttention(nn.Module):
499
positions: torch.Tensor,
500
hidden_states: torch.Tensor,
501
) -> torch.Tensor:
502
+ q_c = None
503
+ kv_lora = None
504
+
505
if self.q_lora_rank is not None:
499
- q_c = self.q_a_proj(hidden_states)[0]
506
+ qkv_lora = self.fused_qkv_a_proj(hidden_states)[0]
507
+ q_c, kv_lora = qkv_lora.split(
508
+ [self.q_lora_rank, self.kv_lora_rank + self.qk_rope_head_dim],
509
+ dim=-1,
510
+ )
511
q_c = self.q_a_layernorm(q_c)
512
q = self.q_b_proj(q_c)[0]
513
else:
514
+ kv_lora = self.kv_a_proj_with_mqa(hidden_states)[0]
515
q = self.q_proj(hidden_states)[0]
504
- kv_c, k_pe = self.kv_a_proj_with_mqa(hidden_states)[0].split(
505
- [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
506
- kv_c_normed = self.kv_a_layernorm(kv_c.contiguous())
516
+
517
+ kv_c, k_pe = kv_lora.split([self.kv_lora_rank, self.qk_rope_head_dim],
518
+ dim=-1)
519
+ kv_c_normed = self.kv_a_layernorm(kv_c)
520
521
q = q.view(-1, self.num_local_heads, self.qk_head_dim)
522
# Add head dim of 1 to k_pe
@@ -837,6 +850,8 @@ class DeepseekV2ForCausalLM(nn.Module, SupportsPP, MixtureOfExperts):
850
# (param_name, shard_name, shard_id)
851
("gate_up_proj", "gate_proj", 0),
852
("gate_up_proj", "up_proj", 1),
853
+ ("fused_qkv_a_proj", "q_a_proj", 0),
854
+ ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
855
]
856
857
# Params for weights, fp8 weight scales, fp8 activation scales
@@ -871,6 +886,12 @@ class DeepseekV2ForCausalLM(nn.Module, SupportsPP, MixtureOfExperts):
886
if (("mlp.experts." in name) and name not in params_dict):
887
continue
888
name = name.replace(weight_name, param_name)
889
+
890
+ # QKV fusion is optional, fall back to normal
891
+ # weight loading if it's not enabled
892
+ if ((param_name == "fused_qkv_a_proj")
893
+ and name not in params_dict):
894
+ continue
895
# Skip loading extra bias for GPTQ models.
896
if name.endswith(".bias") and name not in params_dict:
897
continue