dexifried
Replace with tiny-router trainer (ZeroGPU/H200)
3bfff54
Raw
History Blame Contribute Delete
6.16 kB
from __future__ import annotations
from typing import Any
import torch
from torch import nn
from transformers import AutoModel
from tiny_router.config import RouterModelConfig
from tiny_router.constants import HEAD_LABELS
class TinyRouterModel(nn.Module):
def __init__(self, config: RouterModelConfig):
super().__init__()
self.config = config
self.encoder = AutoModel.from_pretrained(config.encoder_name)
hidden_size = self.encoder.config.hidden_size
if config.pooling_type not in {"mean", "attention"}:
raise ValueError(f"Unsupported pooling type: {config.pooling_type}")
self.dropout = nn.Dropout(config.dropout)
self.pooling_type = config.pooling_type
if self.pooling_type == "attention":
self.attention_pool = nn.Linear(hidden_size, 1)
self.action_embedding = nn.Embedding(len(config.action_vocab), config.structured_hidden_dim)
self.outcome_embedding = nn.Embedding(len(config.outcome_vocab), config.structured_hidden_dim)
self.recency_projection = nn.Sequential(
nn.Linear(1, config.recency_embed_dim),
nn.ReLU(),
nn.Linear(config.recency_embed_dim, config.recency_embed_dim),
)
structured_dim = (config.structured_hidden_dim * 2) + config.recency_embed_dim
canonical_head_order = list(HEAD_LABELS.keys())
extra_heads = [head for head in config.label_maps if head not in HEAD_LABELS]
self.head_order = [head for head in canonical_head_order if head in config.label_maps] + extra_heads
self.head_dependencies = {
head: self.head_order[:idx]
for idx, head in enumerate(self.head_order)
}
self.dependency_projections = nn.ModuleDict()
self.heads = nn.ModuleDict(
self._build_heads(hidden_size + structured_dim)
)
self.loss_fns = {head: nn.CrossEntropyLoss() for head in config.label_maps}
def _build_heads(self, base_dim: int) -> dict[str, nn.Linear]:
heads: dict[str, nn.Linear] = {}
for head, labels in self.config.label_maps.items():
input_dim = base_dim
if self.config.use_head_dependencies and self.head_dependencies[head]:
dependency_input_dim = sum(
len(self.config.label_maps[source])
for source in self.head_dependencies[head]
)
self.dependency_projections[head] = nn.Sequential(
nn.Linear(dependency_input_dim, self.config.dependency_hidden_dim),
nn.Tanh(),
nn.Dropout(self.config.dropout),
)
input_dim += self.config.dependency_hidden_dim
heads[head] = nn.Linear(input_dim, len(labels))
return heads
def pool_hidden_state(self, hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
if self.pooling_type == "attention":
scores = self.attention_pool(hidden_state).squeeze(-1)
scores = scores.masked_fill(attention_mask == 0, -1e4)
weights = torch.softmax(scores, dim=-1).unsqueeze(-1)
return torch.sum(hidden_state * weights, dim=1)
mask = attention_mask.unsqueeze(-1).expand(hidden_state.size()).float()
summed = torch.sum(hidden_state * mask, dim=1)
counts = torch.clamp(mask.sum(dim=1), min=1e-6)
return summed / counts
def forward(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor,
previous_action_id: torch.Tensor,
previous_outcome_id: torch.Tensor,
log_recency_seconds: torch.Tensor,
has_interaction: torch.Tensor,
has_recency: torch.Tensor | None = None,
head_loss_weights: dict[str, float] | None = None,
**kwargs: Any,
) -> dict[str, Any]:
outputs = self.encoder(input_ids=input_ids, attention_mask=attention_mask)
pooled = self.pool_hidden_state(outputs.last_hidden_state, attention_mask)
pooled = self.dropout(pooled)
structured_mask = has_interaction.float().unsqueeze(-1)
recency_mask_source = has_interaction if has_recency is None else has_recency
recency_mask = recency_mask_source.float().unsqueeze(-1)
action_emb = self.action_embedding(previous_action_id) * structured_mask
outcome_emb = self.outcome_embedding(previous_outcome_id) * structured_mask
recency_emb = self.recency_projection(log_recency_seconds.unsqueeze(-1)) * recency_mask
combined = torch.cat([pooled, action_emb, outcome_emb, recency_emb], dim=-1)
logits = {}
prior_head_probs: dict[str, torch.Tensor] = {}
for head in self.head_order:
head_inputs = [combined]
if self.config.use_head_dependencies and self.head_dependencies[head]:
dependency_input = torch.cat(
[prior_head_probs[source] for source in self.head_dependencies[head]],
dim=-1,
)
dependency_features = self.dependency_projections[head](dependency_input)
head_inputs.append(dependency_features)
classifier_input = torch.cat(head_inputs, dim=-1)
head_logits = self.heads[head](classifier_input)
logits[head] = head_logits
# Keep the supervision for earlier heads local to their own loss while still
# exposing their predictions to later heads.
prior_head_probs[head] = torch.softmax(head_logits.detach(), dim=-1)
result: dict[str, Any] = {"logits": logits}
total_loss = None
for head in self.config.label_maps:
label_key = f"labels_{head}"
if label_key not in kwargs:
continue
loss = self.loss_fns[head](logits[head], kwargs[label_key])
if head_loss_weights:
loss = loss * float(head_loss_weights.get(head, 1.0))
total_loss = loss if total_loss is None else total_loss + loss
result["loss"] = total_loss
return result