multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
6b3a024 verified
Raw
History Blame Contribute Delete
5.85 kB
# 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
@torch.no_grad()
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"
)