@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 69ff99fdcd
Fork 0

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, 21:00 UTC

[Core] Optimizing cross-attention `QKVParallelLinear` computation (#12325)

Signed-off-by: NickLucche <nlucches@redhat.com> Signed-off-by: NickLucche <nick@nlucches-4xa100.c.openshift-330514.internal> Co-authored-by: NickLucche <nick@nlucches-4xa100.c.openshift-330514.internal>

Nicolò Lucchesi committed Mar 6, 2025 at 10:37 UTC 69ff99fdcddf4d6dbcebf5f750b121dd171b86a3
4 files changed +121 -44
vllm/model_executor/layers/linear.py
+95
@@ -1227,3 +1227,98 @@ class RowParallelLinear(LinearBase):
1227 s += f", tp_size={self.tp_size}"
1228 s += f", reduce_results={self.reduce_results}"
1229 return s
1230 +
1231 +
1232 +class QKVCrossParallelLinear(torch.nn.Module):
1233 +
1234 + def __init__(self,
1235 + hidden_size: int,
1236 + head_size: int,
1237 + total_num_heads: int,
1238 + total_num_kv_heads: Optional[int] = None,
1239 + bias: bool = True,
1240 + skip_bias_add: bool = False,
1241 + params_dtype: Optional[torch.dtype] = None,
1242 + quant_config: Optional[QuantizationConfig] = None,
1243 + prefix: str = ""):
1244 + super().__init__()
1245 + # Empty placeholders for loading as a single module.
1246 + self.weight = torch.nn.Parameter()
1247 + set_weight_attrs(self.weight, {
1248 + "weight_loader": self.weight_loader_weight,
1249 + })
1250 + # Use a dictionary to avoid submodules parameters auto-registration:
1251 + # drop-in replacement for a `QKVParallelLinear` module.
1252 + self.proj = dict()
1253 + self.proj["q_proj_decoder"] = ColumnParallelLinear(
1254 + input_size=hidden_size,
1255 + output_size=total_num_heads * head_size,
1256 + bias=bias,
1257 + quant_config=quant_config,
1258 + skip_bias_add=skip_bias_add,
1259 + params_dtype=params_dtype,
1260 + prefix=f"{prefix}.q_proj_decoder")
1261 +
1262 + self.proj["kv_proj_encoder"] = QKVParallelLinear(
1263 + hidden_size=hidden_size,
1264 + head_size=head_size,
1265 + total_num_heads=0,
1266 + total_num_kv_heads=total_num_kv_heads,
1267 + bias=bias,
1268 + quant_config=quant_config,
1269 + skip_bias_add=skip_bias_add,
1270 + params_dtype=params_dtype,
1271 + prefix=f"{prefix}.kv_proj_encoder")
1272 +
1273 + # `kv_proj_encoder.num_kv_heads` accounts for sharding with tp>1.
1274 + self.kv_size = self.kv_proj_encoder.num_kv_heads * head_size
1275 +
1276 + if bias:
1277 + self.bias = torch.nn.Parameter()
1278 + set_weight_attrs(self.bias, {
1279 + "weight_loader": self.weight_loader_bias,
1280 + })
1281 +
1282 + @property
1283 + def q_proj_decoder(self):
1284 + return self.proj["q_proj_decoder"]
1285 +
1286 + @property
1287 + def kv_proj_encoder(self):
1288 + return self.proj["kv_proj_encoder"]
1289 +
1290 + def forward(self, decoder_hidden_states, encoder_hidden_states):
1291 + q, _ = self.q_proj_decoder(decoder_hidden_states)
1292 + if encoder_hidden_states is None:
1293 + # Encoder KV already cached.
1294 + k = None
1295 + v = None
1296 + else:
1297 + # Prefill phase, encoder KV cached here.
1298 + kv_enc, _ = self.kv_proj_encoder(encoder_hidden_states)
1299 + # Split kv in half
1300 + k, v = kv_enc.split(self.kv_size, dim=-1)
1301 + return q, k, v
1302 +
1303 + def weight_loader_weight(self,
1304 + param: torch.nn.Parameter,
1305 + loaded_weight: torch.Tensor,
1306 + loaded_shard_id: Optional[str] = None):
1307 + # NOTE Use QKV/ColumnParallel weight_loader, ignore placeholder param.
1308 + param = self.q_proj_decoder.weight if loaded_shard_id == "q" \
1309 + else self.kv_proj_encoder.weight
1310 + param.weight_loader(
1311 + param,
1312 + loaded_weight) if loaded_shard_id == "q" else param.weight_loader(
1313 + param, loaded_weight, loaded_shard_id)
1314 +
1315 + def weight_loader_bias(self,
1316 + param: torch.nn.Parameter,
1317 + loaded_weight: torch.Tensor,
1318 + loaded_shard_id: Optional[str] = None):
1319 + param = self.q_proj_decoder.bias if loaded_shard_id == "q" \
1320 + else self.kv_proj_encoder.bias
1321 + param.weight_loader(
1322 + param,
1323 + loaded_weight) if loaded_shard_id == "q" else param.weight_loader(
1324 + param, loaded_weight, loaded_shard_id)
\ No newline at end of file
vllm/model_executor/models/bart.py
+13 -26
@@ -31,6 +31,7 @@ from vllm.config import CacheConfig, LoRAConfig, VllmConfig
31 from vllm.distributed import get_tensor_model_parallel_world_size
32 from vllm.model_executor.layers.activation import get_act_fn
33 from vllm.model_executor.layers.linear import (ColumnParallelLinear,
34 + QKVCrossParallelLinear,
35 QKVParallelLinear,
36 RowParallelLinear)
37 from vllm.model_executor.layers.logits_processor import LogitsProcessor
@@ -169,7 +170,7 @@ class BartEncoderAttention(nn.Module):
170 # Number of KV heads is less than TP size, so we replicate
171 # the KV heads across multiple tensor parallel GPUs.
172 assert tp_world_size % self.total_num_kv_heads == 0
172 - self.num_kv_heads = max(1, self.total_num_kv_heads // tp_world_size)
173 + self.num_kv_heads = self.num_heads
174 self.q_size = self.num_heads * self.head_dim
175 self.kv_size = self.num_kv_heads * self.head_dim
176
@@ -248,7 +249,7 @@ class BartDecoderSelfAttention(nn.Module):
249 # Number of KV heads is less than TP size, so we replicate
250 # the KV heads across multiple tensor parallel GPUs.
251 assert tp_world_size % self.total_num_kv_heads == 0
251 - self.num_kv_heads = max(1, self.total_num_kv_heads // tp_world_size)
252 + self.num_kv_heads = self.num_heads
253 self.q_size = self.num_heads * self.head_dim
254 self.kv_size = self.num_kv_heads * self.head_dim
255
@@ -299,14 +300,14 @@ class BartCrossAttention(nn.Module):
300 f" and `num_heads`: {num_heads}).")
301 self.scaling = self.head_dim**-0.5
302
302 - self.qkv_proj = QKVParallelLinear(
303 - self.d_model,
304 - self.d_model // self.total_num_heads,
305 - self.total_num_heads,
306 - self.total_num_kv_heads,
307 - bias=bias,
308 - quant_config=quant_config,
309 - )
303 + # TP sharding sizes is accounted for within "*Parallel" layers.
304 + self.qkv_proj = QKVCrossParallelLinear(self.d_model,
305 + self.d_model //
306 + self.total_num_heads,
307 + self.total_num_heads,
308 + self.total_num_kv_heads,
309 + bias,
310 + quant_config=quant_config)
311
312 self.out_proj = RowParallelLinear(
313 embed_dim,
@@ -327,10 +328,7 @@ class BartCrossAttention(nn.Module):
328 # Number of KV heads is less than TP size, so we replicate
329 # the KV heads across multiple tensor parallel GPUs.
330 assert tp_world_size % self.total_num_kv_heads == 0
330 - self.num_kv_heads = max(1, self.total_num_kv_heads // tp_world_size)
331 - self.q_size = self.num_heads * self.head_dim
332 - self.kv_size = self.num_kv_heads * self.head_dim
333 -
331 + self.num_kv_heads = self.num_heads # No GQA in bart
332 self.attn = Attention(self.num_heads,
333 self.head_dim,
334 self.scaling,
@@ -347,18 +345,7 @@ class BartCrossAttention(nn.Module):
345 ) -> torch.Tensor:
346 """Input shape: Batch x Time x Channel"""
347
350 - # (afeldman-nm 2024/07/22) TODO:
351 - # Need a more efficient solution for q/k/v
352 - qkv_dec, _ = self.qkv_proj(decoder_hidden_states)
353 - q, _, _ = qkv_dec.split([self.q_size, self.kv_size, self.kv_size],
354 - dim=-1)
355 - if encoder_hidden_states is None:
356 - k = None
357 - v = None
358 - else:
359 - qkv_enc, _ = self.qkv_proj(encoder_hidden_states)
360 - _, k, v = qkv_enc.split([self.q_size, self.kv_size, self.kv_size],
361 - dim=-1)
348 + q, k, v = self.qkv_proj(decoder_hidden_states, encoder_hidden_states)
349
350 attn_output = self.attn(q, k, v)
351
vllm/model_executor/models/mllama.py
+12 -17
@@ -43,6 +43,7 @@ from vllm.forward_context import get_forward_context
43 from vllm.logger import init_logger
44 from vllm.model_executor.layers.layernorm import RMSNorm
45 from vllm.model_executor.layers.linear import (ColumnParallelLinear,
46 + QKVCrossParallelLinear,
47 QKVParallelLinear,
48 RowParallelLinear)
49 from vllm.model_executor.layers.logits_processor import LogitsProcessor
@@ -798,21 +799,22 @@ class MllamaTextCrossAttention(nn.Module):
799 self.config = config
800 self.pipeline_parallel_rank = get_pp_group().rank_in_group
801 self.tensor_parallel_size = get_tp_group().world_size
801 - self.num_heads = self.config.num_attention_heads
802 + self.num_heads = config.num_attention_heads
803 + self.num_key_value_heads = config.num_key_value_heads
804 +
805 self.num_local_heads = self.num_heads // self.tensor_parallel_size
803 - self.num_key_value_heads = self.config.num_key_value_heads
806 self.num_local_key_value_heads = \
807 self.num_key_value_heads // self.tensor_parallel_size
806 - self.dropout = config.dropout
808 self.hidden_size = config.hidden_size
809 self.head_dim = config.hidden_size // self.num_heads
810 + self.num_key_value_heads = config.num_key_value_heads
811 +
812 self.layer_idx = layer_idx
813 self.num_key_value_groups = self.num_heads // self.num_key_value_heads
814 self.q_local_size = self.num_local_heads * self.head_dim
815 self.kv_local_size = self.num_local_key_value_heads * self.head_dim
816
814 - # TODO: change to Q/KV separate linear after #7448 is merged
815 - self.qkv_proj = QKVParallelLinear(
817 + self.qkv_proj = QKVCrossParallelLinear(
818 self.hidden_size,
819 self.head_dim,
820 self.num_heads,
@@ -821,6 +823,7 @@ class MllamaTextCrossAttention(nn.Module):
823 quant_config=quant_config,
824 prefix=f"{prefix}.qkv_proj",
825 )
826 +
827 self.o_proj = RowParallelLinear(
828 self.num_heads * self.head_dim,
829 self.hidden_size,
@@ -851,21 +854,12 @@ class MllamaTextCrossAttention(nn.Module):
854 kv_range_for_decode: Optional[List[Tuple[int, int]]],
855 cross_attention_states: Optional[torch.Tensor],
856 ) -> torch.Tensor:
854 - qkv_dec, _ = self.qkv_proj(hidden_states)
855 - q, _, _ = qkv_dec.split(
856 - [self.q_local_size, self.kv_local_size, self.kv_local_size],
857 - dim=-1)
858 - if cross_attention_states is None:
859 - k = None
860 - v = None
861 - else:
862 - qkv_enc, _ = self.qkv_proj(cross_attention_states)
863 - _, k, v = qkv_enc.split(
864 - [self.q_local_size, self.kv_local_size, self.kv_local_size],
865 - dim=-1)
857 + q, k, v = self.qkv_proj(hidden_states, cross_attention_states)
858 + if cross_attention_states is not None:
859 k = k.view(-1, self.num_local_key_value_heads, self.head_dim)
860 v = v.view(-1, self.num_local_key_value_heads, self.head_dim)
861 k = self.k_norm(k)
862 +
863 q = q.view(-1, self.num_local_heads, self.head_dim)
864 q = self.q_norm(q)
865
@@ -889,6 +883,7 @@ class MllamaTextCrossAttention(nn.Module):
883 kv_cache = self.attn.kv_cache[self.pipeline_parallel_rank]
884 attn_metadata: AttentionMetadata = get_forward_context().attn_metadata
885 # Skip writing kv-cache for the initial profiling run.
886 + # TODO (NickLucche) replace with custom attn bias and use standard attn
887 if len(kv_cache.shape) > 1:
888 i = torch.ones(1, dtype=torch.float32)
889 if self.attn.backend in (_Backend.FLASH_ATTN,
vllm/model_executor/models/utils.py
+1 -1
@@ -650,4 +650,4 @@ def cast_overflow_tensors(
650 if tensors.isinf().any() or tensors.isnan().any():
651 clamp_value = torch.finfo(tensors.dtype).max - offset
652 tensors = torch.clamp(tensors, min=-clamp_value, max=clamp_value)
653 - return tensors
653 + return tensors
\ No newline at end of file