main
py 680 lines 24.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import itertools
5 from collections.abc import Iterable
6 from contextlib import contextmanager
7 from typing import TYPE_CHECKING, Any, TypeVar
8
9 import torch
10 import torch.nn as nn
11
12 from vllm.config import VllmConfig
13 from vllm.logger import init_logger
14 from vllm.model_executor.layers.activation import get_act_fn
15 from vllm.model_executor.models.config import VerifyAndUpdateConfig
16 from vllm.transformers_utils.config import (
17 try_get_dense_modules,
18 )
19 from vllm.transformers_utils.repo_utils import get_hf_file_bytes
20
21 from .interfaces import supports_multimodal
22 from .interfaces_base import VllmModelForPooling, is_pooling_model
23
24 if TYPE_CHECKING:
25 from vllm.config import ModelConfig, VllmConfig
26 from vllm.model_executor.layers.pooler import Pooler
27
28 _T = TypeVar("_T", bound=nn.Module)
29
30 logger = init_logger(__name__)
31
32 _GENERATE_SUFFIXES = [
33 "ForCausalLM",
34 "ForConditionalGeneration",
35 "ChatModel",
36 "LMHeadModel",
37 ]
38
39
40 def _load_st_projector(model_config: "ModelConfig") -> nn.Module | None:
41 """Load Sentence-Transformers Dense projection layers."""
42 dense_modules = try_get_dense_modules(
43 model_config.model, revision=model_config.revision
44 )
45
46 if dense_modules is None:
47 return None
48
49 try:
50 layers = []
51 for layer_config in dense_modules:
52 folder = layer_config["folder"]
53 linear = nn.Linear(
54 layer_config["in_features"],
55 layer_config["out_features"],
56 bias=layer_config.get("bias", True),
57 dtype=model_config.head_dtype,
58 )
59 if not _load_dense_weights(linear, folder, model_config):
60 continue
61 layers.append(linear)
62 if act_name := layer_config.get("activation_function"):
63 layers.append(get_act_fn(act_name))
64 return nn.Sequential(*layers).to(dtype=model_config.head_dtype)
65 except Exception:
66 logger.exception("ST projector loading failed")
67
68 return None
69
70
71 def _load_dense_weights(
72 linear: nn.Linear, folder: str, model_config: "ModelConfig"
73 ) -> bool:
74 """Load weights using vLLM's weight_loader pattern."""
75 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
76
77 for filename in ["model.safetensors", "pytorch_model.bin"]:
78 file_path = f"{folder}/{filename}" if folder else filename
79
80 try:
81 file_bytes = get_hf_file_bytes(
82 file_path, model_config.model, model_config.revision
83 )
84 if not file_bytes:
85 continue
86
87 if filename.endswith(".safetensors"):
88 from safetensors.torch import load as load_safetensors
89
90 state_dict = load_safetensors(file_bytes)
91 else:
92 import io
93
94 state_dict = torch.load(
95 io.BytesIO(file_bytes), map_location="cpu", weights_only=True
96 )
97
98 for weight_key in ["weight", "linear.weight", "dense.weight"]:
99 if weight_key in state_dict:
100 weight_loader = getattr(
101 linear.weight, "weight_loader", default_weight_loader
102 )
103 weight_loader(linear.weight, state_dict[weight_key])
104
105 bias_key = weight_key.replace("weight", "bias")
106 if linear.bias is not None and bias_key in state_dict:
107 bias_loader = getattr(
108 linear.bias, "weight_loader", default_weight_loader
109 )
110 bias_loader(linear.bias, state_dict[bias_key])
111 return True
112 except Exception:
113 logger.exception("Failed to load %s", filename)
114 continue
115
116 return False
117
118
119 def _get_pooling_model_name(orig_model_name: str, pooling_suffix: str) -> str:
120 model_name = orig_model_name
121
122 for generate_suffix in _GENERATE_SUFFIXES:
123 model_name = model_name.removesuffix(generate_suffix)
124
125 return model_name + pooling_suffix
126
127
128 def _create_pooling_model_cls(orig_cls: type[_T]) -> type[_T]:
129 # Lazy import
130 from vllm.model_executor.layers.logits_processor import LogitsProcessor
131 from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
132
133 from .utils import AutoWeightsLoader, StageMissingLayer, no_init_weights
134
135 class ModelForPooling(orig_cls, VllmModelForPooling): # type: ignore[valid-type,misc]
136 is_pooling_model = True
137
138 def __init__(
139 self,
140 *,
141 vllm_config: "VllmConfig",
142 prefix: str = "",
143 **kwargs: Any,
144 ) -> None:
145 with no_init_weights(
146 self,
147 lambda mod: StageMissingLayer("output", mod),
148 targets=(LogitsProcessor, ParallelLMHead),
149 ):
150 super().__init__( # type: ignore[safe-super]
151 vllm_config=vllm_config, prefix=prefix, **kwargs
152 )
153
154 # Used by SEQ_CLS_LOAD_METHODS
155 self.vllm_config = vllm_config
156
157 # If the model already defines a pooler instance, don't overwrite it
158 pooler = getattr(self, "pooler", None)
159 multimodal_model: object = self
160 if not pooler and supports_multimodal(multimodal_model):
161 # Try to get the pooler from the LM backbone
162 language_model = multimodal_model.get_language_model()
163 if hasattr(language_model, "pooler"):
164 pooler = language_model.pooler
165
166 if not pooler:
167 pooler = self._init_pooler(vllm_config, prefix=prefix)
168
169 self.pooler = pooler
170
171 def _init_pooler(
172 self,
173 vllm_config: "VllmConfig",
174 prefix: str = "",
175 ) -> "Pooler":
176 raise NotImplementedError
177
178 def _load_pooling_model_weights(
179 self, weights: Iterable[tuple[str, torch.Tensor]]
180 ):
181 params_dict = dict(self.named_parameters())
182 # Use model-owned remapping only for the probe membership check
183 # (e.g. Gemma4 model.language_model.* -> language_model.model.*)
184 # so the "" / "model." scan can early-exit. Forwarded names stay
185 # unmapped to avoid double-applying the mapper in the parent loader.
186 hf_to_vllm_mapper = getattr(self, "hf_to_vllm_mapper", None)
187
188 # We support loading from both `*ForCausalLM` and `*Model`
189 candidate_prefixes = ["", "model."]
190 target_prefix = ""
191
192 seen_weights = list[tuple[str, torch.Tensor]]()
193 for name, loaded_weight in weights:
194 # Clone because the iterator may reuse the tensor buffer
195 seen_weights.append((name, loaded_weight.clone()))
196
197 lookup_name = name
198 if hf_to_vllm_mapper is not None:
199 mapped = hf_to_vllm_mapper._map_name(name)
200 if mapped is None:
201 continue
202 lookup_name = mapped
203
204 try:
205 target_prefix = next(
206 prefix
207 for prefix in candidate_prefixes
208 if prefix + lookup_name in params_dict
209 )
210 break
211 except StopIteration:
212 # The weight might not exist on the model
213 # (to be handled by AutoWeightsLoader)
214 pass
215
216 if target_prefix:
217 target_model = self
218 for attr in target_prefix.split("."):
219 if attr:
220 target_model = getattr(self, attr)
221
222 logger.info(
223 "Mapping weights to %s as they are "
224 "relative to this model instead of %s.",
225 target_model._get_name(),
226 self._get_name(),
227 )
228
229 # Lazy chain so buffer-reusing weight iterators (e.g.
230 # runai_streamer) are consumed one tensor at a time.
231 mapped_weights = (
232 (target_prefix + name, weight)
233 for name, weight in itertools.chain(seen_weights, weights)
234 )
235
236 def default_load_weights(weights):
237 loader = AutoWeightsLoader(self)
238 mapper = getattr(self, "hf_to_vllm_mapper", None)
239 return loader.load_weights(weights, mapper=mapper)
240
241 load_weights = getattr(super(), "load_weights", default_load_weights)
242 return load_weights(mapped_weights)
243
244 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
245 return self._load_pooling_model_weights(weights)
246
247 return ModelForPooling
248
249
250 def as_embedding_model(cls: type[_T]) -> type[_T]:
251 """Subclass an existing vLLM model to support embeddings.
252
253 By default, the embeddings of the whole prompt are extracted from the
254 normalized hidden state corresponding to the last token.
255
256 Note:
257 We assume that no extra layers are added to the original model;
258 please implement your own model if this is not the case.
259
260 """
261 # Avoid modifying existing embedding models
262 if is_pooling_model(cls):
263 return cls
264
265 # Lazy import
266 from vllm.model_executor.layers.pooler import DispatchPooler
267
268 class ModelForEmbedding(_create_pooling_model_cls(cls)): # type: ignore[misc]
269 def _init_pooler(
270 self,
271 vllm_config: "VllmConfig",
272 prefix: str = "",
273 ) -> "Pooler":
274 pooler_config = vllm_config.model_config.pooler_config
275 assert pooler_config is not None
276
277 return DispatchPooler.for_embedding(pooler_config)
278
279 ModelForEmbedding.__name__ = _get_pooling_model_name(cls.__name__, "ForEmbedding")
280
281 return ModelForEmbedding
282
283
284 def _resolve_num_labels(hf_config: Any, text_config: Any) -> int:
285 """Resolve the label count for a sequence classification head.
286
287 ``PreTrainedConfig.num_labels`` is derived from ``id2label``, which always
288 carries a default of two entries. Composite configs (such as multimodal
289 checkpoints) declare their label space on the top-level config, so reading
290 ``num_labels`` from ``get_text_config()`` silently returns that default and
291 builds a score head of the wrong size.
292
293 Prefer the top-level config whenever it declares a label space of its own,
294 mirroring the ``classifier_from_token`` / ``method`` lookups in
295 ``as_seq_cls_model``.
296 """
297 if text_config is hf_config:
298 return hf_config.num_labels
299
300 from transformers import PreTrainedConfig
301
302 if hf_config.num_labels != PreTrainedConfig().num_labels:
303 return hf_config.num_labels
304 return text_config.num_labels
305
306
307 def as_seq_cls_model(cls: type[_T]) -> type[_T]:
308 """Subclass an existing vLLM model to support classify and score tasks.
309
310 By default, the class probabilities are extracted from the softmaxed
311 hidden state corresponding to the last token.
312
313 Note:
314 We assume that the classification head is a single linear layer
315 stored as the attribute `score` of the top-level model;
316 please implement your own model if this is not the case.
317
318 """
319 # Avoid modifying existing classification models
320 if is_pooling_model(cls):
321 return cls
322
323 # Lazy import
324 from vllm.model_executor.layers.linear import ReplicatedLinear
325 from vllm.model_executor.layers.pooler import DispatchPooler
326 from vllm.model_executor.models.interfaces import SupportsCrossEncoding
327
328 from .utils import maybe_prefix
329
330 class ModelForSequenceClassification(
331 _create_pooling_model_cls(cls), # type: ignore[misc]
332 SupportsCrossEncoding,
333 ):
334 def _init_pooler(
335 self,
336 vllm_config: "VllmConfig",
337 prefix: str = "",
338 ) -> "Pooler":
339 hf_config = vllm_config.model_config.hf_config
340 text_config = hf_config.get_text_config()
341 model_config = vllm_config.model_config
342
343 # Check if score weights are derived online from LM head
344 # (same condition as load_weights branch)
345 tokens = getattr(
346 hf_config,
347 "classifier_from_token",
348 getattr(text_config, "classifier_from_token", None),
349 )
350 method = getattr(
351 hf_config,
352 "method",
353 getattr(text_config, "method", None),
354 )
355
356 # Online conversion: no score weights in checkpoint, don't
357 # quantize (small output_dim breaks FP8/Marlin tile alignment).
358 # Checkpoint-based: respect the model's quant_config.
359 quant_config = (
360 None
361 if (tokens is not None or method is not None)
362 else vllm_config.quant_config
363 )
364
365 self.score = ReplicatedLinear(
366 model_config.get_hidden_size(),
367 _resolve_num_labels(hf_config, text_config),
368 bias=False,
369 params_dtype=model_config.head_dtype,
370 quant_config=quant_config,
371 return_bias=False,
372 prefix=maybe_prefix(prefix, "score"),
373 )
374
375 pooler_config = vllm_config.model_config.pooler_config
376 assert pooler_config is not None
377
378 return DispatchPooler.for_seq_cls(pooler_config, classifier=self.score)
379
380 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
381 hf_config = self.config
382 text_config = hf_config.get_text_config()
383 tokens = getattr(
384 hf_config,
385 "classifier_from_token",
386 getattr(text_config, "classifier_from_token", None),
387 )
388 method = getattr(hf_config, "method", getattr(text_config, "method", None))
389
390 def auto_set_score_bias(weights):
391 for name, weight in weights:
392 if name == "score.bias":
393 device = self.score.weight.device
394 dtype = self.score.weight.dtype
395 bias = weight.to(device).to(dtype)
396 self.score.bias = torch.nn.Parameter(bias)
397 self.score.skip_bias_add = False
398 else:
399 yield name, weight
400
401 weights = auto_set_score_bias(weights)
402 if tokens is None and method is None:
403 return super().load_weights(weights)
404 else:
405 # Online convert ForCausalLM into
406 # ForSequenceClassification model.
407 return seq_cls_model_loader(self, weights)
408
409 ModelForSequenceClassification.__name__ = _get_pooling_model_name(
410 cls.__name__, "ForSequenceClassification"
411 )
412
413 return ModelForSequenceClassification
414
415
416 class SequenceClassificationConfig(VerifyAndUpdateConfig):
417 @staticmethod
418 def verify_and_update_config(vllm_config: "VllmConfig") -> None:
419 hf_config = vllm_config.model_config.hf_config
420 text_config = hf_config.get_text_config()
421 method = getattr(hf_config, "method", getattr(text_config, "method", None))
422 tokens = getattr(
423 hf_config,
424 "classifier_from_token",
425 getattr(text_config, "classifier_from_token", None),
426 )
427
428 if method is None:
429 return
430
431 assert tokens is not None
432 assert method in SEQ_CLS_LOAD_METHODS, f"method {method} not supported"
433
434 if method == "from_2_way_softmax":
435 assert len(tokens) == 2
436 hf_config.num_labels = 1
437 text_config.num_labels = 1
438 else:
439 hf_config.num_labels = len(tokens)
440 text_config.num_labels = len(tokens)
441
442 # `llm as reranker` defaults to not using separating token.
443 use_sep_token = getattr(text_config, "use_sep_token", False)
444 text_config.use_sep_token = use_sep_token
445
446
447 def _get_language_model_for_seq_cls(model: nn.Module) -> nn.Module:
448 """Get the language model component for sequence classification conversion.
449 For VLMs, returns the inner language model. For standard LLMs, returns model itself.
450 """
451 multimodal_model: object = model
452 if supports_multimodal(multimodal_model):
453 try:
454 lm = multimodal_model.get_language_model()
455 if lm is not model:
456 return lm
457 except Exception:
458 pass
459
460 for attr_name in ("language_model", "lm", "text_model"):
461 if hasattr(model, attr_name):
462 candidate = getattr(model, attr_name)
463 if (
464 isinstance(candidate, nn.Module)
465 and candidate is not model
466 and hasattr(candidate, "model")
467 ):
468 return candidate
469
470 for name, child in model.named_children():
471 child_name = type(child).__name__
472 if ("ForCausalLM" in child_name or "LMHead" in child_name) and hasattr(
473 child, "model"
474 ):
475 return child
476
477 return model
478
479
480 @contextmanager
481 def _disable_seq_cls_loading_on_inner_model(language_model, is_vlm: bool):
482 """Context manager to temporarily disable sequence classification loading
483 on inner VLM models to prevent recursive seq_cls_model_loader calls.
484 """
485 if not is_vlm:
486 yield
487 return
488
489 inner_hf_config = getattr(language_model, "config", None)
490 if inner_hf_config is None:
491 yield
492 return
493
494 inner_text_config = inner_hf_config.get_text_config()
495 original_method = getattr(inner_text_config, "method", None)
496 original_tokens = getattr(inner_text_config, "classifier_from_token", None)
497 original_hf_tokens = getattr(inner_hf_config, "classifier_from_token", None)
498
499 try:
500 if original_method is not None:
501 inner_text_config.method = None
502 if original_tokens is not None:
503 inner_text_config.classifier_from_token = None
504 if original_hf_tokens is not None:
505 inner_hf_config.classifier_from_token = None
506 yield
507 finally:
508 if original_method is not None:
509 inner_text_config.method = original_method
510 if original_tokens is not None:
511 inner_text_config.classifier_from_token = original_tokens
512 if original_hf_tokens is not None:
513 inner_hf_config.classifier_from_token = original_hf_tokens
514
515
516 def load_weights_using_from_2_way_softmax(
517 model, weights: Iterable[tuple[str, torch.Tensor]]
518 ):
519 # refer to https://huggingface.co/Qwen/Qwen3-Reranker-0.6B/discussions/3
520 from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
521 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
522
523 model_config = model.vllm_config.model_config
524 hf_config = model.config
525 text_config = hf_config.get_text_config()
526
527 tokens: list[str] = getattr(
528 hf_config,
529 "classifier_from_token",
530 getattr(text_config, "classifier_from_token", []),
531 )
532 assert len(tokens) == 2
533
534 language_model = _get_language_model_for_seq_cls(model)
535 is_vlm = language_model is not model
536 using_vlm_head = is_vlm and hasattr(language_model, "score")
537
538 language_model.lm_head = ParallelLMHead(
539 text_config.vocab_size,
540 text_config.hidden_size,
541 )
542 if text_config.tie_word_embeddings:
543 # embed_tokens is the assumed name for input embeddings. If the model does not
544 # have this attribute, we fall back to get_input_embeddings(), which is used by
545 # the Transformers modeling backend.
546 text_backbone = language_model.model
547 embed_tokens = (
548 text_backbone.embed_tokens
549 if hasattr(text_backbone, "embed_tokens")
550 else text_backbone.get_input_embeddings()
551 )
552 language_model.lm_head = language_model.lm_head.tie_weights(embed_tokens)
553
554 with _disable_seq_cls_loading_on_inner_model(language_model, is_vlm):
555 loaded_weights = model._load_pooling_model_weights(weights)
556
557 from vllm.tokenizers import get_tokenizer
558
559 tokenizer = get_tokenizer(
560 model_config.tokenizer,
561 revision=model_config.tokenizer_revision,
562 tokenizer_mode=model_config.tokenizer_mode,
563 trust_remote_code=model_config.trust_remote_code,
564 )
565
566 false_id = tokenizer.convert_tokens_to_ids(tokens[0])
567 true_id = tokenizer.convert_tokens_to_ids(tokens[1])
568 lm_head_weight = language_model.lm_head.weight
569 score_weight = lm_head_weight.data[[true_id]].to(
570 torch.float32
571 ) - lm_head_weight.data[[false_id]].to(torch.float32)
572
573 score_layer = language_model.score if using_vlm_head else model.score
574 param = score_layer.weight
575 weight_loader = getattr(param, "weight_loader", default_weight_loader)
576 weight_loader(param, score_weight)
577
578 del language_model.lm_head
579
580 score_weight_name = (
581 "language_model.score.weight" if using_vlm_head else "score.weight"
582 )
583 loaded_weights.add(score_weight_name)
584
585 lm_head_name: str | None = "lm_head.weight"
586 if hf_to_vllm_mapper := getattr(model, "hf_to_vllm_mapper", None):
587 lm_head_name = hf_to_vllm_mapper._map_name(lm_head_name)
588 if lm_head_name is not None:
589 loaded_weights.discard(lm_head_name)
590 return loaded_weights
591
592
593 def load_weights_no_post_processing(model, weights: Iterable[tuple[str, torch.Tensor]]):
594 from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
595 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
596
597 model_config = model.vllm_config.model_config
598 text_config = model.config.get_text_config()
599
600 tokens: list[str] = getattr(text_config, "classifier_from_token", [])
601 assert len(tokens) > 0
602
603 language_model = _get_language_model_for_seq_cls(model)
604 is_vlm = language_model is not model
605 using_vlm_head = is_vlm and hasattr(language_model, "score")
606
607 language_model.lm_head = ParallelLMHead(
608 text_config.vocab_size,
609 text_config.hidden_size,
610 )
611 if text_config.tie_word_embeddings:
612 # embed_tokens is the assumed name for input embeddings. If the model does not
613 # have this attribute, we fall back to get_input_embeddings(), which is used by
614 # the Transformers modeling backend.
615 text_backbone = language_model.model
616 embed_tokens = (
617 text_backbone.embed_tokens
618 if hasattr(text_backbone, "embed_tokens")
619 else text_backbone.get_input_embeddings()
620 )
621 language_model.lm_head = language_model.lm_head.tie_weights(embed_tokens)
622
623 with _disable_seq_cls_loading_on_inner_model(language_model, is_vlm):
624 # Bypass ModelForSequenceClassification to avoid infinite recursion
625 loaded_weights = model._load_pooling_model_weights(weights)
626
627 from vllm.tokenizers import get_tokenizer
628
629 tokenizer = get_tokenizer(
630 model_config.tokenizer,
631 revision=model_config.tokenizer_revision,
632 tokenizer_mode=model_config.tokenizer_mode,
633 trust_remote_code=model_config.trust_remote_code,
634 )
635
636 token_ids = [tokenizer.convert_tokens_to_ids(t) for t in tokens]
637 score_weight = language_model.lm_head.weight.data[token_ids]
638
639 score_layer = language_model.score if using_vlm_head else model.score
640 param = score_layer.weight
641 weight_loader = getattr(param, "weight_loader", default_weight_loader)
642 weight_loader(param, score_weight)
643
644 del language_model.lm_head
645
646 score_weight_name = (
647 "language_model.score.weight" if using_vlm_head else "score.weight"
648 )
649 loaded_weights.add(score_weight_name)
650
651 lm_head_name: str | None = "lm_head.weight"
652 if hf_to_vllm_mapper := getattr(model, "hf_to_vllm_mapper", None):
653 lm_head_name = hf_to_vllm_mapper._map_name(lm_head_name)
654 if lm_head_name is not None:
655 loaded_weights.discard(lm_head_name)
656 return loaded_weights
657
658
659 SEQ_CLS_LOAD_METHODS = {
660 "from_2_way_softmax": load_weights_using_from_2_way_softmax,
661 "no_post_processing": load_weights_no_post_processing,
662 }
663
664
665 def seq_cls_model_loader(model, weights: Iterable[tuple[str, torch.Tensor]]):
666 # Online convert ForCausalLM into ForSequenceClassification model.
667 # - from_2_way_softmax:
668 # - Qwen3ForCausalLM
669 # - Qwen3-Reranker
670 # - Qwen2ForCausalLM
671 # - mxbai-rerank-v2
672 # - no_post_processing:
673 # - GemmaForCausalLM
674 # - bge-reranker-v2-gemma
675
676 hf_config = model.vllm_config.model_config.hf_config
677 text_config = hf_config.get_text_config()
678 method = getattr(hf_config, "method", getattr(text_config, "method", None))
679 assert method in SEQ_CLS_LOAD_METHODS, f"method {method} not supported"
680 return SEQ_CLS_LOAD_METHODS[method](model, weights)