Spaces:
Running on Zero
Running on Zero
| # V-SPLADE | |
| # Copyright (c) 2026-present NAVER Corp. | |
| # Apache-2.0 | |
| """ | |
| UnifiedRetriever -- V_SPLADE core model. | |
| Combines: | |
| VBert encoder + Sparse head (SPLADE) + BOW query encoder + Losses | |
| Training objective (per forward call): | |
| L_total = NCE(q, p, hn) + lambda_p * FLOPS(p) + lambda_cap * CaptionPushUp | |
| The model expects a passage (image) and an optional caption; the query is | |
| encoded as a Bag-of-Words vocabulary indicator. The sparse passage | |
| representation is supervised by: | |
| - NCE in-batch ranking against the query (and hard negatives, if any) | |
| - FLOPS regularization to encourage sparsity | |
| - Caption-gated token supervision: pull passage activations toward overlapping | |
| caption-vocabulary tokens. | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| from models.encoder import build_encoder | |
| from models.pooling import Pooling | |
| from models.head import SparseHead | |
| from models.query_encoder import ( | |
| build_query_encoder, | |
| BOWQueryEncoder, | |
| InferenceFreeQueryEncoder, | |
| ) | |
| from models.losses import FLOPSLoss, NCELoss, CaptionPushUpLoss, ZipfianPushUpLoss # noqa: F401 | |
| def compute_logits(q, p, hn, temperature): | |
| """Compute in-batch negative logits and labels. | |
| Args: | |
| q: (B, V) query sparse reps | |
| p: (B, V) passage sparse reps | |
| hn: (B*num_hn, V) hard-negative passage reps, or None | |
| temperature: scaling factor for the inner product | |
| Returns: | |
| logits: (B, B [+ B*num_hn]) score matrix | |
| labels: (B,) target index for cross-entropy | |
| """ | |
| B = q.size(0) | |
| device = q.device | |
| all_p = torch.cat([p, hn], dim=0) if hn is not None else p | |
| logits = (q @ all_p.t()) / temperature | |
| logits = logits.clamp(min=-100, max=100) # prevent exp() overflow in cross_entropy | |
| labels = torch.arange(B, device=device) | |
| return logits, labels | |
| # Default pooling for V_SPLADE (single backbone): max over sequence positions. | |
| DEFAULT_POOLING = "max" | |
| class RetrievalOutput: | |
| """Container for forward() outputs.""" | |
| loss: Optional[torch.Tensor] = None | |
| rank_loss: Optional[torch.Tensor] = None | |
| reg_loss: Optional[torch.Tensor] = None # total reg (passage FLOPS + caption FLOPS) | |
| reg_loss_p: Optional[torch.Tensor] = None # passage FLOPS only | |
| reg_loss_cap: Optional[torch.Tensor] = None # caption FLOPS only | |
| cap_loss: Optional[torch.Tensor] = None # caption-gated token supervision loss | |
| cap_sparse_rank_loss: Optional[torch.Tensor] = None # caption-gated ranking loss | |
| # Sparsity / retrieval-cost diagnostics (no_grad, mean over batch). | |
| train_p_nnz: Optional[torch.Tensor] = None # avg nonzero per passage | |
| train_p_max: Optional[torch.Tensor] = None # avg max|p| per passage | |
| train_flops_qd: Optional[torch.Tensor] = None # sum_v mean|q|_v * mean|p|_v | |
| train_cap_bow_killed_pct: Optional[torch.Tensor] = None # % bow-active tokens zeroed by model | |
| query_reps: Optional[torch.Tensor] = None | |
| passage_reps: Optional[torch.Tensor] = None | |
| hard_neg_reps: Optional[torch.Tensor] = None | |
| debug_tokens: Optional[dict] = None | |
| class UnifiedRetriever(nn.Module): | |
| """V_SPLADE retriever: VBert encoder + SPLADE sparse head + (BOW | Li-LSR) query encoder.""" | |
| def __init__( | |
| self, | |
| encoder_type: str = "vbert", | |
| pooling_type: str = None, | |
| head_type: str = "sparse", | |
| query_encoder_type: str = "bow", | |
| # Model paths | |
| model_name: str = "", | |
| lm_head_model: str = None, | |
| # Training config | |
| temperature: float = 1.0, | |
| # Caption-gated token supervision | |
| cap_weight: float = 0.0, | |
| cap_sparse_rank_weight: float = 0.0, | |
| cap_loss_mode: str = "logsigmoid_h", | |
| overlap_type: str = "passage_mean", | |
| p_mean_alpha: float = 1.0, | |
| use_zipfian_pushup: bool = False, | |
| push_focus_tau: float = 1.0, | |
| # Regularization weights | |
| reg_weight_p: float = 0.0, | |
| reg_weight_cap: float = 0.0, | |
| # SPLADE pooling | |
| splade_pooling: str = "max", | |
| # Encoder-specific kwargs | |
| lm_head_lora_r: int = 32, | |
| encoder_lora_r: int = 32, | |
| lm_head_full: bool = False, | |
| # Li-LSR query encoder kwargs | |
| query_lsr_lora_r: int = 0, | |
| query_lsr_activation: str = "relu", | |
| # Misc | |
| **kwargs, | |
| ): | |
| super().__init__() | |
| # V_SPLADE only supports the sparse head with a single (vbert) backbone. | |
| assert encoder_type == "vbert", "V_SPLADE only supports the vbert encoder." | |
| assert head_type == "sparse", "V_SPLADE only supports the sparse head." | |
| if pooling_type is None: | |
| pooling_type = DEFAULT_POOLING | |
| self.encoder_type = encoder_type | |
| self.head_type = head_type | |
| self.query_encoder_type = query_encoder_type | |
| self.splade_pooling = splade_pooling | |
| # Build encoder. | |
| encoder_kwargs = dict( | |
| model_name=model_name, | |
| lm_head_model=lm_head_model or model_name, | |
| lm_head_lora_r=lm_head_lora_r, | |
| encoder_lora_r=encoder_lora_r, | |
| lm_head_full=lm_head_full, | |
| **kwargs, | |
| ) | |
| self.encoder = build_encoder(encoder_type, **encoder_kwargs) | |
| # Build pooling and sparse head. | |
| self.pooling = Pooling(pooling_type) | |
| lm_head = self.encoder.get_lm_head() | |
| self.head = SparseHead(lm_head, self.encoder.hidden_size) | |
| self.vocab_size = self.encoder.vocab_size | |
| self.hidden_size = self.encoder.hidden_size | |
| # Build query encoder: BOW (cheapest) or Li-LSR (inference-free learned). | |
| assert query_encoder_type in ("bow", "li_lsr"), ( | |
| "V_SPLADE only supports the BOW or Li-LSR query encoders." | |
| ) | |
| if query_encoder_type == "li_lsr": | |
| embed_layer = self.encoder.get_text_embeddings() | |
| if embed_layer is not None: | |
| embed_weight = embed_layer.weight | |
| else: | |
| # Fallback: use lm_head weights (tied with embed_tokens in causal LMs). | |
| _lm_head = self.encoder.get_lm_head() | |
| embed_weight = _lm_head.weight if _lm_head is not None else None | |
| self.query_encoder = build_query_encoder( | |
| "li_lsr", | |
| vocab_size=self.vocab_size, | |
| hidden_size=self.hidden_size, | |
| embed_weight=embed_weight, | |
| lora_r=query_lsr_lora_r, | |
| activation=query_lsr_activation, | |
| ) | |
| else: | |
| self.query_encoder = build_query_encoder("bow", vocab_size=self.vocab_size) | |
| # Loss / regularization config. | |
| self.temperature = temperature | |
| self.cap_weight = cap_weight | |
| self.cap_sparse_rank_weight = cap_sparse_rank_weight | |
| self.cap_loss_mode = cap_loss_mode | |
| self.overlap_type = overlap_type | |
| self.reg_weight_p = reg_weight_p | |
| self.reg_weight_cap = reg_weight_cap | |
| self.loss_fn = nn.CrossEntropyLoss() | |
| self.reg_fn = FLOPSLoss() | |
| if cap_weight > 0: | |
| if use_zipfian_pushup: | |
| self.cap_push_up = ZipfianPushUpLoss( | |
| push_focus_tau=push_focus_tau, | |
| p_mean_alpha=p_mean_alpha, | |
| cap_loss_mode=cap_loss_mode, | |
| ) | |
| else: | |
| self.cap_push_up = CaptionPushUpLoss( | |
| cap_loss_mode, p_mean_alpha=p_mean_alpha, | |
| ) | |
| else: | |
| self.cap_push_up = None | |
| # Special-token mask for the VBert vocabulary. Prevents tokens such as | |
| # [CLS]/[SEP]/[PAD]/[MASK] (and out-of-MLM image tokens) from activating | |
| # in the sparse representation. | |
| _special_ids = [ | |
| 50280, # [UNK] | |
| 50281, # [CLS] | |
| 50282, # [SEP] | |
| 50283, # [PAD] | |
| 50284, # [MASK] | |
| # Additional image/layout tokens beyond MLM head vocab (50368); | |
| # kept here for safety in case downstream code changes vocab_size. | |
| *range(50368, 50408), | |
| ] | |
| mask = torch.ones(self.vocab_size) | |
| for sid in _special_ids: | |
| if sid < self.vocab_size: | |
| mask[sid] = 0.0 | |
| self.register_buffer("special_token_mask", mask, persistent=False) | |
| def from_hf_export(cls, hf_dir: str, | |
| query_lsr_activation: str = "softplus", | |
| dtype: torch.dtype = torch.bfloat16) -> "UnifiedRetriever": | |
| """Build an empty V-SPLADE retriever shell from a V-SPLADE HF export. | |
| Only the module *structure* is created (random weights); call | |
| :func:`models.load_hf_export` afterwards to populate every tensor | |
| from the export's `model.safetensors` in one pass. | |
| """ | |
| from models.encoder import VBertEncoder | |
| instance = cls.__new__(cls) | |
| nn.Module.__init__(instance) | |
| instance.encoder_type = "vbert" | |
| instance.head_type = "sparse" | |
| instance.query_encoder_type = "li_lsr" | |
| instance.splade_pooling = "max" | |
| instance.temperature = 1.0 | |
| instance.cap_weight = 0.0 | |
| instance.cap_sparse_rank_weight = 0.0 | |
| instance.cap_loss_mode = "logsigmoid_h" | |
| instance.overlap_type = "passage_mean" | |
| instance.reg_weight_p = 0.0 | |
| instance.reg_weight_cap = 0.0 | |
| instance.cap_push_up = None | |
| instance.loss_fn = nn.CrossEntropyLoss() | |
| instance.reg_fn = FLOPSLoss() | |
| # Encoder + sparse head (shares the encoder's LM head via SparseHead). | |
| instance.encoder = VBertEncoder.from_hf_export(hf_dir, dtype=dtype) | |
| instance.vocab_size = instance.encoder.vocab_size | |
| instance.hidden_size = instance.encoder.hidden_size | |
| instance.pooling = Pooling("max") | |
| instance.head = SparseHead(instance.encoder.get_lm_head(), | |
| instance.encoder.hidden_size) | |
| # Li-LSR inference-free query encoder (softplus activation, LoRA off). | |
| embed_layer = instance.encoder.get_text_embeddings() | |
| embed_weight = (embed_layer.weight if embed_layer is not None | |
| else instance.encoder.get_lm_head().weight) | |
| instance.query_encoder = build_query_encoder( | |
| "li_lsr", | |
| vocab_size=instance.vocab_size, | |
| hidden_size=instance.hidden_size, | |
| embed_weight=embed_weight, | |
| lora_r=0, | |
| activation=query_lsr_activation, | |
| ) | |
| # Special-token mask (same as training). | |
| _special_ids = [50280, 50281, 50282, 50283, 50284, *range(50368, 50408)] | |
| mask = torch.ones(instance.vocab_size) | |
| for sid in _special_ids: | |
| if sid < instance.vocab_size: | |
| mask[sid] = 0.0 | |
| instance.register_buffer("special_token_mask", mask, persistent=False) | |
| for p in instance.parameters(): | |
| p.requires_grad = False | |
| return instance.eval() | |
| def gradient_checkpointing_enable(self, gradient_checkpointing_kwargs=None): | |
| self.encoder.gradient_checkpointing_enable(gradient_checkpointing_kwargs) | |
| # ---- Encoding helpers ---------------------------------------------------- | |
| def _encode_passage_sparse(self, **kwargs): | |
| """Encode passage -> (h_raw, w_sparse) via encoder + sparse head.""" | |
| hidden, mask = self.encoder.encode_passage(**kwargs) | |
| return self._apply_sparse_head(hidden, mask) | |
| def _encode_text_sparse(self, **kwargs): | |
| """Encode text (e.g. caption) -> (h_raw, w_sparse).""" | |
| hidden, mask = self.encoder.encode_text(**kwargs) | |
| return self._apply_sparse_head(hidden, mask) | |
| def _apply_sparse_head(self, hidden_states, attention_mask): | |
| """SPLADE sparse head: LM head (per position) -> max/mean pool over sequence.""" | |
| scale = self.hidden_size ** -0.25 | |
| # max/mean pooling: apply LM head over all positions, then pool. | |
| h = self.head.lm_head(hidden_states) * scale # (B, seq, vocab) | |
| w = torch.log1p(torch.relu(h)) | |
| mask = attention_mask.unsqueeze(-1).to(w.dtype) | |
| if self.splade_pooling == "max": | |
| w_out = (w * mask).max(dim=1).values | |
| h_masked = h * mask + (~mask.bool()) * (-1e9) | |
| h_out = h_masked.max(dim=1).values | |
| else: # mean | |
| seq_len = mask.sum(dim=1, keepdim=True).clamp(min=1) | |
| w_out = (w * mask).sum(dim=1) / seq_len.squeeze(1) | |
| h_out = (h * mask).sum(dim=1) / seq_len.squeeze(1) | |
| # Mask special tokens. | |
| w_out = w_out * self.special_token_mask.to(w_out.dtype) | |
| return h_out, w_out | |
| def _encode_query(self, input_ids, attention_mask): | |
| """Encode query via BOW or Li-LSR (training: forward, eval: lookup).""" | |
| if isinstance(self.query_encoder, InferenceFreeQueryEncoder): | |
| if self.training: | |
| q = self.query_encoder(input_ids, attention_mask) | |
| else: | |
| q = self.query_encoder.encode_with_lookup(input_ids, attention_mask) | |
| else: | |
| q = self.query_encoder(input_ids, attention_mask) | |
| # Apply special-token mask to the query output. | |
| if q.dim() == 2 and q.size(-1) == self.special_token_mask.size(0): | |
| q = q * self.special_token_mask.to(q.dtype).to(q.device) | |
| return q | |
| def encode_query(self, input_ids, attention_mask) -> torch.Tensor: | |
| """Public API for query encoding.""" | |
| return self._encode_query(input_ids, attention_mask) | |
| def encode_passage( | |
| self, input_ids=None, attention_mask=None, | |
| pixel_values=None, pixel_attention_mask=None, | |
| ) -> torch.Tensor: | |
| """Public API for passage encoding.""" | |
| kwargs = {} | |
| if input_ids is not None: | |
| kwargs["input_ids"] = input_ids | |
| if attention_mask is not None: | |
| kwargs["attention_mask"] = attention_mask | |
| if pixel_values is not None: | |
| kwargs["pixel_values"] = pixel_values | |
| if pixel_attention_mask is not None: | |
| kwargs["pixel_attention_mask"] = pixel_attention_mask | |
| _, w = self._encode_passage_sparse(**kwargs) | |
| return w | |
| # ---- Forward ------------------------------------------------------------- | |
| def forward( | |
| self, | |
| query_input_ids, query_attention_mask, | |
| passage_input_ids, passage_attention_mask, | |
| passage_pixel_values=None, passage_pixel_attention_mask=None, | |
| caption_input_ids=None, caption_attention_mask=None, | |
| hard_neg_passage_input_ids=None, hard_neg_passage_attention_mask=None, | |
| hard_neg_passage_pixel_values=None, hard_neg_passage_pixel_attention_mask=None, | |
| **kwargs, | |
| ) -> RetrievalOutput: | |
| """V_SPLADE forward pass. | |
| Computes NCE rank loss + FLOPS regularization + (optional) caption-gated token | |
| loss. Multi-vector / self-distillation / dense / multi-backbone paths | |
| are out of scope for this code release. | |
| """ | |
| has_caption = caption_input_ids is not None | |
| use_cap = ( | |
| self.cap_sparse_rank_weight > 0 | |
| or self.cap_weight > 0 | |
| or self.reg_weight_cap > 0 | |
| ) and has_caption | |
| # ---- Query (BOW) ---- | |
| q_reps = self._encode_query(query_input_ids, query_attention_mask) | |
| # ---- Positive passage ---- | |
| p_kwargs = dict(input_ids=passage_input_ids, attention_mask=passage_attention_mask) | |
| if passage_pixel_values is not None: | |
| p_kwargs["pixel_values"] = passage_pixel_values | |
| if passage_pixel_attention_mask is not None: | |
| p_kwargs["pixel_attention_mask"] = passage_pixel_attention_mask | |
| p_h, p_reps = self._encode_passage_sparse(**p_kwargs) | |
| # ---- Hard negatives (optional) ---- | |
| hn_reps = None | |
| if hard_neg_passage_input_ids is not None: | |
| if hard_neg_passage_pixel_values is not None: | |
| # Image hard negatives. | |
| hn_kwargs = dict( | |
| input_ids=hard_neg_passage_input_ids, | |
| attention_mask=hard_neg_passage_attention_mask, | |
| pixel_values=hard_neg_passage_pixel_values, | |
| pixel_attention_mask=hard_neg_passage_pixel_attention_mask, | |
| ) | |
| _, hn_reps = self._encode_passage_sparse(**hn_kwargs) | |
| else: | |
| # Text hard negatives. | |
| _, hn_reps = self._encode_text_sparse( | |
| input_ids=hard_neg_passage_input_ids, | |
| attention_mask=hard_neg_passage_attention_mask, | |
| ) | |
| # ---- Caption encoding ---- | |
| cap_sparse = None | |
| cap_sparse_raw = None # kept for diagnostics | |
| cap_bow = None | |
| if use_cap: | |
| _, cap_sparse_raw = self._encode_text_sparse( | |
| input_ids=caption_input_ids, attention_mask=caption_attention_mask, | |
| ) | |
| # Use plain caption tokens for BOW if available (so that any instruction | |
| # tokens added by a caption prompt are not counted in the BOW mask). | |
| bow_ids = kwargs.get("caption_bow_input_ids", caption_input_ids) | |
| bow_mask = kwargs.get("caption_bow_attention_mask", caption_attention_mask) | |
| cap_bow = BOWQueryEncoder(self.vocab_size).to(q_reps.device)(bow_ids, bow_mask) | |
| cap_bow = cap_bow * self.special_token_mask.to(cap_bow.dtype).to(cap_bow.device) | |
| cap_sparse = cap_sparse_raw * cap_bow.to(cap_sparse_raw.dtype) | |
| # ---- dtype alignment (BOW query is float32, passage is bf16) ---- | |
| if q_reps.dtype != p_reps.dtype: | |
| q_reps = q_reps.to(p_reps.dtype) | |
| # ---- Ranking loss (NCE) — cross-GPU gather for in-batch negatives ---- | |
| # Symmetric gather across DDP ranks: every gathered query sees | |
| # ``world_size × B`` passages as negatives. The local rank's slice | |
| # keeps gradient via ``all_gather_2d_with_grad``; other ranks are | |
| # detached. Asymmetric gathers (only p) under-train the passage | |
| # encoder because each p sees gradients from only ``B`` queries. | |
| import torch.distributed as _dist_nce | |
| if _dist_nce.is_initialized() and _dist_nce.get_world_size() > 1: | |
| from models.losses import all_gather_2d_with_grad as _gather_nce | |
| q_reps_g = _gather_nce(q_reps) | |
| p_reps_g = _gather_nce(p_reps) | |
| hn_reps_g = _gather_nce(hn_reps) if hn_reps is not None else None | |
| logits, labels = compute_logits( | |
| q_reps_g, p_reps_g, hn_reps_g, self.temperature, | |
| ) | |
| else: | |
| logits, labels = compute_logits( | |
| q_reps, p_reps, hn_reps, self.temperature, | |
| ) | |
| rank_loss = self.loss_fn(logits, labels) | |
| # ---- FLOPS regularization ---- | |
| reg_loss_p = self.reg_weight_p * self.reg_fn(p_reps) | |
| if hn_reps is not None: | |
| reg_loss_p = reg_loss_p + self.reg_weight_p * self.reg_fn(hn_reps) | |
| reg_loss_cap = q_reps.new_tensor(0.0) | |
| if cap_sparse is not None: | |
| reg_loss_cap = self.reg_weight_cap * self.reg_fn(cap_sparse) | |
| reg_loss = reg_loss_p + reg_loss_cap | |
| # ---- Caption loss ---- | |
| cap_loss = q_reps.new_tensor(0.0) | |
| cap_sparse_rank_loss = q_reps.new_tensor(0.0) | |
| if use_cap and self.cap_sparse_rank_weight > 0 and cap_sparse is not None: | |
| # Cross-GPU gather for caption-side ranking too (parallel to the | |
| # passage-side NCE above): caption push-up still uses LOCAL p_reps. | |
| import torch.distributed as _dist_capr | |
| if _dist_capr.is_initialized() and _dist_capr.get_world_size() > 1: | |
| from models.losses import all_gather_2d_with_grad as _gather_capr | |
| q_reps_capr = _gather_capr(q_reps) | |
| cap_sparse_capr = _gather_capr(cap_sparse) | |
| else: | |
| q_reps_capr = q_reps | |
| cap_sparse_capr = cap_sparse | |
| cap_s_logits, cap_s_labels = compute_logits( | |
| q_reps_capr, cap_sparse_capr, None, self.temperature, | |
| ) | |
| cap_sparse_rank_loss = self.loss_fn(cap_s_logits, cap_s_labels) * self.cap_sparse_rank_weight | |
| if use_cap and self.cap_weight > 0 and cap_sparse is not None and self.cap_push_up is not None: | |
| cap_loss = self.cap_push_up(p_h, p_reps, cap_sparse, self.overlap_type) * self.cap_weight | |
| # ---- Total loss ---- | |
| loss = rank_loss + reg_loss + cap_loss + cap_sparse_rank_loss | |
| # ---- Diagnostics (no_grad) ---- | |
| debug_tokens = None | |
| train_p_nnz = train_p_max = train_flops_qd = None | |
| train_cap_bow_killed_pct = None | |
| with torch.no_grad(): | |
| def _topk_lowk(vec, k=5): | |
| """Return (top-k values, indices, low-k nonzero values, indices).""" | |
| nz_mask = vec > 0 | |
| nz_n = int(nz_mask.sum().item()) | |
| if nz_n == 0: | |
| return (torch.empty(0), torch.empty(0, dtype=torch.long), | |
| torch.empty(0), torch.empty(0, dtype=torch.long)) | |
| t_k = min(k, vec.size(0)) | |
| l_k = min(k, nz_n) | |
| top_v, top_i = vec.topk(t_k) | |
| masked = torch.where(nz_mask, vec, torch.full_like(vec, float('inf'))) | |
| low_vals_asc, low_idx_asc = masked.topk(l_k, largest=False) | |
| return top_v.cpu(), top_i.cpu(), low_vals_asc.cpu(), low_idx_asc.cpu() | |
| q0, p0 = q_reps[0], p_reps[0] | |
| q_top_v, q_top_i, q_low_v, q_low_i = _topk_lowk(q0) | |
| p_top_v, p_top_i, p_low_v, p_low_i = _topk_lowk(p0) | |
| debug_tokens = { | |
| "q_indices": q_top_i, "q_values": q_top_v, | |
| "q_low_indices": q_low_i, "q_low_values": q_low_v, | |
| "p_indices": p_top_i, "p_values": p_top_v, | |
| "p_low_indices": p_low_i, "p_low_values": p_low_v, | |
| "q_input_ids": query_input_ids[0].cpu(), | |
| "q_attention_mask": query_attention_mask[0].cpu(), | |
| "q_nonzero": (q0 > 0).sum().item(), | |
| "p_nonzero": (p0 > 0).sum().item(), | |
| } | |
| # Batch sparsity / FLOPs diagnostics. | |
| train_p_nnz = (p_reps > 0).float().sum(-1).mean().detach() | |
| train_p_max = p_reps.max(dim=-1).values.mean().detach() | |
| train_flops_qd = (q_reps.abs().mean(dim=0) * p_reps.abs().mean(dim=0)).sum().detach() | |
| if cap_sparse is not None: | |
| cap0 = cap_sparse[0] | |
| c_top_v, c_top_i, c_low_v, c_low_i = _topk_lowk(cap0) | |
| debug_tokens["cap_indices"] = c_top_i | |
| debug_tokens["cap_values"] = c_top_v | |
| debug_tokens["cap_low_indices"] = c_low_i | |
| debug_tokens["cap_low_values"] = c_low_v | |
| debug_tokens["cap_nonzero"] = (cap0 > 0).sum().item() | |
| # Push-up overlap top-5 / low-5. | |
| p_w_d = p0.detach() | |
| p_mean_d = p_w_d.mean(dim=-1, keepdim=True) | |
| alpha = self.cap_push_up.p_mean_alpha if self.cap_push_up is not None else 1.0 | |
| overlap = (p_w_d + alpha * p_mean_d) * cap0.detach() | |
| overlap_norm = overlap / (overlap.sum() + 1e-8) | |
| ov_top_v, ov_top_i, ov_low_v, ov_low_i = _topk_lowk(overlap_norm) | |
| debug_tokens["pushup_indices"] = ov_top_i | |
| debug_tokens["pushup_values"] = ov_top_v | |
| debug_tokens["pushup_low_indices"] = ov_low_i | |
| debug_tokens["pushup_low_values"] = ov_low_v | |
| # cap_bow_killed_pct: among bow-active caption tokens, what fraction | |
| # did the SPLADE head zero out? (Higher = model treats more cap | |
| # tokens as noise.) | |
| if cap_bow is not None and cap_sparse_raw is not None: | |
| bow_active = (cap_bow > 0).float() | |
| raw_active = (cap_sparse_raw > 0).float() | |
| killed = bow_active * (1.0 - raw_active) | |
| denom = bow_active.sum(-1).clamp(min=1.0) | |
| train_cap_bow_killed_pct = (killed.sum(-1) / denom).mean().detach() | |
| return RetrievalOutput( | |
| loss=loss, | |
| rank_loss=rank_loss, | |
| reg_loss=reg_loss, | |
| reg_loss_p=reg_loss_p, | |
| reg_loss_cap=reg_loss_cap, | |
| cap_loss=cap_loss, | |
| cap_sparse_rank_loss=cap_sparse_rank_loss, | |
| train_p_nnz=train_p_nnz, | |
| train_p_max=train_p_max, | |
| train_flops_qd=train_flops_qd, | |
| train_cap_bow_killed_pct=train_cap_bow_killed_pct, | |
| query_reps=q_reps, | |
| passage_reps=p_reps, | |
| hard_neg_reps=hn_reps, | |
| debug_tokens=debug_tokens, | |
| ) | |