CoolFace
Modelpublic

KaLM-Embedding/KaLM-Reranker-V1-Nano-R2

sourceHugging Faceapache-2.0updated 2d agoView on Hugging Face
2likes92downloads
kalm_reranker.py296 linesDownload Raw Back to root
1from __future__ import annotations2 3from typing import Any, Dict, List, Optional, Sequence, Tuple, Union4 5import numpy as np6import torch7from transformers import AutoModelForSeq2SeqLM, AutoTokenizer8 9try:10    from .kalm_reranker_utils import (11        DEFAULT_INSTRUCTION,12        DEFAULT_SYSTEM_INSTRUCTION,13        answer_token_id,14        build_decoder_text,15        cast_floating_parameters,16        extract_yes_no_logits,17        forward_reranker_model,18        get_encoder,19        pool_encoder_chunks,20        validate_text_pairs,21    )22except ImportError:  # Support ``from kalm_reranker import KaLMReranker``.23    from kalm_reranker_utils import (24        DEFAULT_INSTRUCTION,25        DEFAULT_SYSTEM_INSTRUCTION,26        answer_token_id,27        build_decoder_text,28        cast_floating_parameters,29        extract_yes_no_logits,30        forward_reranker_model,31        get_encoder,32        pool_encoder_chunks,33        validate_text_pairs,34    )35 36 37class KaLMReranker:38    """Score query-document relevance with a KaLM encoder-decoder reranker.39 40    The returned score is ``P(yes)`` after applying a two-class softmax to the41    model's ``yes`` and ``no`` logits.42    """43 44    def __init__(45        self,46        model_name_or_path: str,47        *,48        device: Optional[Union[str, torch.device]] = None,49        dtype: Optional[Union[str, torch.dtype]] = None,50        batch_size: int = 32,51        query_max_length: int = 512,52        max_length: int = 1024,53        chunk_size: Optional[int] = 4,54        instruction: str = DEFAULT_INSTRUCTION,55        system_instruction: str = DEFAULT_SYSTEM_INSTRUCTION,56        **model_kwargs: Any,57    ) -> None:58        if not isinstance(model_name_or_path, str) or not model_name_or_path:59            raise ValueError("model_name_or_path must be a non-empty string.")60        if batch_size <= 0:61            raise ValueError("batch_size must be positive.")62        if query_max_length <= 0 or max_length <= 0:63            raise ValueError("query_max_length and max_length must be positive.")64        if chunk_size is not None and chunk_size <= 0:65            raise ValueError("chunk_size must be positive or None.")66        if not isinstance(instruction, str) or not isinstance(system_instruction, str):67            raise TypeError("instruction and system_instruction must be strings.")68 69        self.device = self._resolve_device(device)70        self.dtype = self._resolve_dtype(dtype, self.device)71        self.batch_size = batch_size72        self.query_max_length = query_max_length73        self.max_length = max_length74        self.chunk_size = chunk_size75        self.instruction = instruction76        self.system_instruction = system_instruction77 78        self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)79        if self.tokenizer.pad_token_id is None:80            if self.tokenizer.eos_token_id is None:81                raise ValueError(82                    "The tokenizer must define a pad token or an EOS token."83                )84            self.tokenizer.pad_token = self.tokenizer.eos_token85        # Final decoder-token indexing assumes right padding, matching training.86        self.tokenizer.padding_side = "right"87 88        self.model = AutoModelForSeq2SeqLM.from_pretrained(89            model_name_or_path,90            dtype=self.dtype,91            **model_kwargs,92        )93        cast_floating_parameters(self.model, self.dtype)94        self.model.to(device=self.device)95        self.model.eval()96 97        self.yes_token_id = self._answer_token_id("yes")98        self.no_token_id = self._answer_token_id("no")99 100    @staticmethod101    def _resolve_device(device: Optional[Union[str, torch.device]]) -> torch.device:102        if device is None:103            device = "cuda" if torch.cuda.is_available() else "cpu"104        resolved = torch.device(device)105        if resolved.type == "cuda" and not torch.cuda.is_available():106            raise RuntimeError("CUDA was requested, but no CUDA device is available.")107        return resolved108 109    @staticmethod110    def _resolve_dtype(111        dtype: Optional[Union[str, torch.dtype]], device: torch.device112    ) -> torch.dtype:113        if dtype is None:114            return torch.bfloat16 if device.type == "cuda" else torch.float32115        if isinstance(dtype, torch.dtype):116            return dtype117        if not isinstance(dtype, str):118            raise TypeError(119                "dtype must be a torch.dtype or a string such as 'bfloat16'."120            )121        normalized = dtype.lower().removeprefix("torch.")122        supported = {123            "bfloat16": torch.bfloat16,124            "bf16": torch.bfloat16,125            "float16": torch.float16,126            "fp16": torch.float16,127            "float32": torch.float32,128            "fp32": torch.float32,129        }130        if normalized not in supported:131            raise ValueError(f"Unsupported dtype: {dtype!r}.")132        return supported[normalized]133 134    def _answer_token_id(self, answer: str) -> int:135        return answer_token_id(self.tokenizer, answer)136 137    def _get_encoder(self):138        return get_encoder(self.model)139 140    @staticmethod141    def _pool_encoder_chunks(142        hidden_states: torch.Tensor,143        attention_mask: torch.Tensor,144        chunk_size: int,145    ) -> Tuple[torch.Tensor, torch.Tensor]:146        return pool_encoder_chunks(hidden_states, attention_mask, chunk_size)147 148    def _decoder_text(self, query: str, instruction: str) -> str:149        return build_decoder_text(150            self.tokenizer,151            query,152            instruction,153            self.system_instruction,154            self.query_max_length,155        )156 157    @staticmethod158    def _validate_pairs(159        pairs: Sequence[Tuple[str, str]],160    ) -> List[Tuple[str, str]]:161        return validate_text_pairs(pairs)162 163    @torch.inference_mode()164    def _predict_batch(165        self, pairs: Sequence[Tuple[str, str]], instruction: str166    ) -> List[float]:167        encoder_texts = [f"<Document>: {document}" for _, document in pairs]168        decoder_texts = [self._decoder_text(query, instruction) for query, _ in pairs]169 170        encoder_batch = self.tokenizer(171            encoder_texts,172            padding=True,173            truncation=True,174            max_length=self.max_length,175            add_special_tokens=False,176            return_tensors="pt",177        ).to(self.device)178        decoder_batch = self.tokenizer(179            decoder_texts,180            padding=True,181            pad_to_multiple_of=8,182            add_special_tokens=False,183            return_tensors="pt",184        ).to(self.device)185 186        outputs = forward_reranker_model(187            self.model,188            input_ids=encoder_batch["input_ids"],189            attention_mask=encoder_batch["attention_mask"],190            decoder_input_ids=decoder_batch["input_ids"],191            decoder_attention_mask=decoder_batch["attention_mask"],192            encoder_chunk_size=self.chunk_size,193        )194        yes_no_logits = extract_yes_no_logits(195            outputs.logits,196            decoder_batch["attention_mask"],197            self.yes_token_id,198            self.no_token_id,199        )200        return torch.softmax(yes_no_logits, dim=-1)[:, 0].cpu().tolist()201 202    def predict(203        self,204        pairs: Sequence[Tuple[str, str]],205        *,206        instruction: Optional[str] = None,207        batch_size: Optional[int] = None,208    ) -> List[float]:209        """Return ``P(yes)`` scores in the same order as ``pairs``."""210        validated_pairs = self._validate_pairs(pairs)211        if not validated_pairs:212            return []213        effective_instruction = self.instruction if instruction is None else instruction214        if not isinstance(effective_instruction, str):215            raise TypeError("instruction must be a string or None.")216        effective_batch_size = self.batch_size if batch_size is None else batch_size217        if not isinstance(effective_batch_size, int) or effective_batch_size <= 0:218            raise ValueError("batch_size must be a positive integer.")219 220        length_sorted_indices = np.argsort(221            [-(len(query) + len(document)) for query, document in validated_pairs]222        )223        sorted_pairs = [validated_pairs[index] for index in length_sorted_indices]224 225        tested_batch_size = effective_batch_size226        first_batch_scores: Optional[List[float]] = None227        while tested_batch_size > 1:228            try:229                first_batch_scores = self._predict_batch(230                    sorted_pairs[: min(len(sorted_pairs), tested_batch_size)],231                    effective_instruction,232                )233                break234            except torch.cuda.OutOfMemoryError:235                if torch.cuda.is_available():236                    torch.cuda.empty_cache()237                tested_batch_size = max(1, tested_batch_size * 3 // 4)238 239        if first_batch_scores is None:240            sorted_scores: List[float] = []241            loop_start = 0242        else:243            sorted_scores = list(first_batch_scores)244            loop_start = tested_batch_size245        try:246            for start in range(loop_start, len(sorted_pairs), tested_batch_size):247                sorted_scores.extend(248                    self._predict_batch(249                        sorted_pairs[start : start + tested_batch_size],250                        effective_instruction,251                    )252                )253        except torch.cuda.OutOfMemoryError as error:254            if torch.cuda.is_available():255                torch.cuda.empty_cache()256            raise RuntimeError(257                "CUDA ran out of memory during reranking. Retry with a smaller "258                "batch_size or shorter max_length."259            ) from error260        inverse_indices = np.argsort(length_sorted_indices)261        return [sorted_scores[index] for index in inverse_indices]262 263    def rank(264        self,265        query: str,266        documents: Sequence[str],267        *,268        instruction: Optional[str] = None,269        top_k: Optional[int] = None,270        batch_size: Optional[int] = None,271    ) -> List[Dict[str, Union[int, float]]]:272        """Rank documents and return ``corpus_id``/``score`` dictionaries."""273        if not isinstance(query, str):274            raise TypeError("query must be a string.")275        if isinstance(documents, (str, bytes)) or not isinstance(documents, Sequence):276            raise TypeError("documents must be a sequence of strings.")277        if any(not isinstance(document, str) for document in documents):278            raise TypeError("every document must be a string.")279        if top_k is not None and (not isinstance(top_k, int) or top_k < 0):280            raise ValueError("top_k must be a non-negative integer or None.")281 282        scores = self.predict(283            [(query, document) for document in documents],284            instruction=instruction,285            batch_size=batch_size,286        )287        rankings: List[Dict[str, Union[int, float]]] = [288            {"corpus_id": corpus_id, "score": score}289            for corpus_id, score in enumerate(scores)290        ]291        rankings.sort(key=lambda item: item["score"], reverse=True)292        return rankings if top_k is None else rankings[:top_k]293 294 295__all__ = ["KaLMReranker"]296