| """Predicted bounded spans over completed causal segments.""" |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
|
|
| import torch |
| from torch import nn |
|
|
| from strata.modeling.ph_pat.config import PHPATConfig |
| from strata.modeling.ph_pat.segment_commit import SegmentLayout |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class SpanCompilerOutput: |
| nodes: torch.Tensor |
| scores: torch.Tensor |
| starts: torch.Tensor |
| ends: torch.Tensor |
| segment_ids: torch.Tensor |
| valid_mask: torch.Tensor |
| boundary_logits: torch.Tensor |
|
|
|
|
| class SpanCompiler(nn.Module): |
| def __init__(self, config: PHPATConfig) -> None: |
| super().__init__() |
| self.config = config |
| self.start = nn.Linear(config.d_model, config.d_model, bias=False) |
| self.end = nn.Linear(config.d_model, config.d_model, bias=False) |
| self.pool = nn.Linear(config.d_model, config.d_model, bias=False) |
| self.score = nn.Sequential( |
| nn.Linear(3 * config.d_model, config.d_model), |
| nn.SiLU(), |
| nn.Linear(config.d_model, 1), |
| ) |
| self.boundary = nn.Linear(config.d_model, 2) |
| relative_starts, relative_ends = _candidate_bounds(0, config.segment_size, config.max_span_width, torch.device("cpu")) |
| self.register_buffer("relative_starts", relative_starts, persistent=False) |
| self.register_buffer("relative_ends", relative_ends, persistent=False) |
|
|
| def forward(self, hidden: torch.Tensor, layout: SegmentLayout) -> SpanCompilerOutput: |
| batch, _seq_len, dim = hidden.shape |
| per_segment = self.config.spans_per_segment |
| prefix = torch.cat((hidden.new_zeros(batch, 1, dim), hidden.cumsum(dim=1)), dim=1) |
| boundary_logits = self.boundary(hidden) |
| segment_offsets = layout.starts.view(1, layout.segment_count, 1) |
| candidate_starts = segment_offsets + self.relative_starts.view(1, 1, -1) |
| candidate_ends = segment_offsets + self.relative_ends.view(1, 1, -1) |
| in_sequence = candidate_ends < hidden.shape[1] |
| safe_starts = candidate_starts.clamp_max(hidden.shape[1] - 1) |
| safe_ends = candidate_ends.clamp_max(hidden.shape[1] - 1) |
| row = torch.arange(batch, device=hidden.device).view(batch, 1, 1) |
| start_score = boundary_logits[row, safe_starts.expand(batch, -1, -1), 0] |
| end_score = boundary_logits[row, safe_ends.expand(batch, -1, -1), 1] |
| candidate_valid = layout.segment_valid.unsqueeze(-1) & in_sequence |
| candidate_scores = (start_score + end_score).masked_fill(~candidate_valid, torch.finfo(hidden.dtype).min) |
| take = min(per_segment, candidate_scores.shape[-1]) |
| top_scores, top_idx = torch.topk(candidate_scores, k=take, dim=-1) |
| expanded_starts = candidate_starts.expand(batch, -1, -1) |
| expanded_ends = candidate_ends.expand(batch, -1, -1) |
| top_starts = expanded_starts.gather(2, top_idx).clamp_max(hidden.shape[1] - 1) |
| top_ends = expanded_ends.gather(2, top_idx).clamp_max(hidden.shape[1] - 1) |
| valid = candidate_valid.expand(batch, -1, -1).gather(2, top_idx) |
| batch_index = torch.arange(batch, device=hidden.device).view(batch, 1, 1) |
| span_sum = prefix[batch_index, top_ends + 1] - prefix[batch_index, top_starts] |
| pooled = span_sum / (top_ends - top_starts + 1).to(hidden.dtype).unsqueeze(-1) |
| start_hidden = hidden[batch_index, top_starts] |
| end_hidden = hidden[batch_index, top_ends] |
| top_nodes = (self.start(start_hidden) + self.end(end_hidden) + self.pool(pooled)) / 3.0 |
| selected_features = torch.cat((start_hidden, end_hidden, pooled), dim=-1) |
| top_scores = top_scores + self.score(selected_features).squeeze(-1) |
|
|
| if take < per_segment: |
| pad = per_segment - take |
| top_nodes = torch.cat((top_nodes, hidden.new_zeros(batch, layout.segment_count, pad, dim)), dim=2) |
| top_scores = torch.cat((top_scores, hidden.new_full((batch, layout.segment_count, pad), torch.finfo(hidden.dtype).min)), dim=2) |
| top_starts = torch.cat((top_starts, torch.zeros(batch, layout.segment_count, pad, device=hidden.device, dtype=torch.long)), dim=2) |
| top_ends = torch.cat((top_ends, torch.zeros(batch, layout.segment_count, pad, device=hidden.device, dtype=torch.long)), dim=2) |
| valid = torch.cat((valid, torch.zeros(batch, layout.segment_count, pad, device=hidden.device, dtype=torch.bool)), dim=2) |
| segment_ids = torch.arange(layout.segment_count, device=hidden.device).view(1, -1, 1).expand(batch, -1, per_segment) |
|
|
| return SpanCompilerOutput( |
| nodes=top_nodes.flatten(1, 2), |
| scores=top_scores.flatten(1, 2), |
| starts=top_starts.flatten(1, 2), |
| ends=top_ends.flatten(1, 2), |
| segment_ids=segment_ids.flatten(1, 2), |
| valid_mask=valid.flatten(1, 2), |
| boundary_logits=boundary_logits, |
| ) |
|
|
|
|
| def _candidate_bounds(start: int, end: int, max_width: int, device: torch.device) -> tuple[torch.Tensor, torch.Tensor]: |
| starts: list[int] = [] |
| ends: list[int] = [] |
| for left in range(start, end): |
| for right in range(left, min(end, left + max_width)): |
| starts.append(left) |
| ends.append(right) |
| return torch.tensor(starts, device=device), torch.tensor(ends, device=device) |
|
|
|
|
| __all__ = ["SpanCompiler", "SpanCompilerOutput"] |
|
|