Spaces:
Running on Zero
Running on Zero
| # V-SPLADE | |
| # Copyright (c) 2026-present NAVER Corp. | |
| # Apache-2.0 | |
| """ | |
| Query encoder strategies. | |
| V_SPLADE quality variant uses an inference-free *Learned Sparse Retriever* | |
| query encoder (``li_lsr``): a frozen copy of the backbone word embeddings, | |
| optionally adapted with a low-rank LoRA, followed by a 1-dim projection | |
| and an activation. At inference time the projection becomes a | |
| ``vocab_size``-entry lookup table, so encoding a query is just an | |
| ``Embedding(input_ids)`` β no transformer forward. | |
| The simpler ``bow`` encoder turns the query into a binary token-presence | |
| indicator over the vocab. Useful for smoke tests and the cheapest | |
| inference-time setting. | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| from enum import Enum | |
| from typing import Optional | |
| class QueryEncoderType(Enum): | |
| BOW = "bow" | |
| LI_LSR = "li_lsr" | |
| class InferenceFreeQueryEncoder(nn.Module): | |
| """Li-LSR style inference-free sparse query encoder. | |
| Training: token_id β embedding (frozen + LoRA) β projection(Dβ1) β | |
| activation β scatter into a vocab-dim vector. | |
| Inference: token_id β precomputed lookup table (no forward pass). | |
| """ | |
| def __init__( | |
| self, | |
| embed_weight: torch.Tensor, | |
| vocab_size: int, | |
| hidden_size: int, | |
| lora_r: int = 0, | |
| activation: str = "relu", | |
| ): | |
| super().__init__() | |
| assert activation in ("relu", "softplus"), f"Unknown activation: {activation}" | |
| self.activation = activation | |
| # Frozen copy of backbone word embeddings. | |
| self.embeddings = nn.Embedding(vocab_size, hidden_size) | |
| self.embeddings.weight.data.copy_(embed_weight.detach()) | |
| self.embeddings.weight.requires_grad = False | |
| # Optional LoRA on the embedding lookup. | |
| self.use_lora = lora_r > 0 | |
| if self.use_lora: | |
| self.lora_A = nn.Parameter(torch.randn(vocab_size, lora_r) * 0.01) | |
| self.lora_B = nn.Parameter(torch.zeros(lora_r, hidden_size)) | |
| # Projection: hidden_size β 1 (fully tuned). | |
| self.projection = nn.Linear(hidden_size, 1, bias=True) | |
| self.vocab_size = vocab_size | |
| self._lookup_table: Optional[torch.Tensor] = None | |
| def _activate(self, x: torch.Tensor) -> torch.Tensor: | |
| if self.activation == "relu": | |
| return torch.log1p(torch.relu(x)) | |
| return torch.nn.functional.softplus(x) | |
| def _valid_mask(self, input_ids: torch.Tensor) -> torch.Tensor: | |
| return input_ids < self.vocab_size | |
| def _get_embed(self, input_ids: torch.Tensor) -> torch.Tensor: | |
| safe_ids = input_ids.clamp(max=self.vocab_size - 1) | |
| e = self.embeddings(safe_ids) | |
| if self.use_lora: | |
| e = e + self.lora_A[safe_ids] @ self.lora_B | |
| return e | |
| def forward( | |
| self, input_ids: torch.Tensor, attention_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """Training forward. Returns (B, vocab_size) sparse vector.""" | |
| valid = self._valid_mask(input_ids) | |
| mask = attention_mask.float() * valid.float() | |
| scores = self.projection(self._get_embed(input_ids)).squeeze(-1) | |
| scores = self._activate(scores) * mask | |
| B = input_ids.size(0) | |
| safe_ids = input_ids.clamp(max=self.vocab_size - 1) | |
| out = torch.zeros(B, self.vocab_size, device=input_ids.device, dtype=scores.dtype) | |
| out.scatter_add_(1, safe_ids, scores) | |
| return out | |
| def build_lookup_table(self) -> torch.Tensor: | |
| device = self.embeddings.weight.device | |
| all_ids = torch.arange(self.vocab_size, device=device) | |
| e = self._get_embed(all_ids.unsqueeze(0)) | |
| s = self.projection(e).squeeze(-1).squeeze(0) | |
| self._lookup_table = self._activate(s) | |
| return self._lookup_table | |
| def encode_with_lookup( | |
| self, input_ids: torch.Tensor, attention_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """Inference: pure lookup, no embedding/projection forward.""" | |
| if self._lookup_table is None: | |
| self.build_lookup_table() | |
| valid = self._valid_mask(input_ids) | |
| mask = attention_mask.float() * valid.float() | |
| safe_ids = input_ids.clamp(max=self.vocab_size - 1) | |
| scores = self._lookup_table.to(input_ids.device)[safe_ids] * mask | |
| B = input_ids.size(0) | |
| out = torch.zeros(B, self.vocab_size, device=input_ids.device, dtype=scores.dtype) | |
| out.scatter_add_(1, safe_ids, scores) | |
| return out | |
| class BOWQueryEncoder(nn.Module): | |
| """Bag-of-Words: binary token indicator over the vocab.""" | |
| def __init__(self, vocab_size: int): | |
| super().__init__() | |
| self.vocab_size = vocab_size | |
| def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor: | |
| B, _ = input_ids.shape | |
| device = input_ids.device | |
| bow = torch.zeros(B, self.vocab_size, device=device, dtype=torch.bfloat16) | |
| safe_ids = input_ids.clamp(0, self.vocab_size - 1) * attention_mask.long() | |
| bow.scatter_(1, safe_ids, 1.0) | |
| bow[:, 0] = 0.0 # clear padding token (id=0) accumulation | |
| return bow | |
| def build_query_encoder( | |
| query_encoder_type: str, | |
| vocab_size: int, | |
| hidden_size: int = 768, | |
| embed_weight: Optional[torch.Tensor] = None, | |
| lora_r: int = 0, | |
| activation: str = "relu", | |
| **kwargs, | |
| ) -> nn.Module: | |
| if query_encoder_type == "bow": | |
| return BOWQueryEncoder(vocab_size) | |
| if query_encoder_type == "li_lsr": | |
| if embed_weight is None: | |
| raise ValueError("li_lsr requires embed_weight from the encoder") | |
| return InferenceFreeQueryEncoder( | |
| embed_weight, vocab_size, hidden_size, | |
| lora_r=lora_r, activation=activation, | |
| ) | |
| raise ValueError( | |
| f"Unknown query_encoder_type: {query_encoder_type}. Choose: bow | li_lsr" | |
| ) | |