Source code for core.nn.multitask_readout

from collections.abc import Mapping

import torch
import torch.nn as nn

from ibl_bwb_eval.tasks import ReadoutSpec


[docs] class MultitaskReadout(nn.Module): """Linear readout with one head per task, routed by integer index. Args: dim: Dimensionality of the incoming output embeddings. readout_specs: Task name to :class:`ibl_bwb_eval.tasks.ReadoutSpec`. Keys name the projections and the entries of the returned dicts. task_index: Task name to the integer written into ``output_readout_index``. Routing uses these, not ``ReadoutSpec.id``, which is a string. """ def __init__( self, dim: int, readout_specs: Mapping[str, ReadoutSpec], task_index: Mapping[str, int], ): super().__init__() missing = set(readout_specs) - set(task_index) if missing: raise ValueError(f"No routing index for readouts: {sorted(missing)}") self.readout_specs = readout_specs # ``projections`` is load-bearing: checkpoints store these as # ``readout.projections.<task_name>.{weight,bias}`` and single-task models # graft a head out of them by that key. self.projections = nn.ModuleDict({}) self._readout_id_to_name = {} for readout_name, readout_spec in self.readout_specs.items(): self.projections[readout_name] = nn.Linear(dim, readout_spec.dim) self._readout_id_to_name[task_index[readout_name]] = readout_name
[docs] def forward( self, output_embs: torch.Tensor, output_readout_index: torch.Tensor, unpack_output: bool = False, ) -> dict[str, torch.Tensor] | list[dict[str, torch.Tensor]]: """Project padded output embeddings through their per-task heads. Args: output_embs: Output embeddings, ``(batch, n_out, dim)``. output_readout_index: Routing index per output token, ``(batch, n_out)``. unpack_output: Return one dict per batch sample instead of one dict with every sample's queries concatenated. Returns: ``{task_name: (total_queries, n_channels)}``, or a list of such dicts with ``(n_queries, n_channels)`` entries when ``unpack_output`` is set. """ outputs = [{} for _ in range(output_embs.shape[0])] if unpack_output else {} for readout_id in output_readout_index.unique().tolist(): readout_name = self._readout_id_to_name.get(readout_id, None) # ids with no head are padding, or tasks excluded from this readout if readout_name is None: continue mask = output_readout_index == readout_id task_output = self.projections[readout_name](output_embs[mask]) if unpack_output: # scatter this task's rows back to the samples they came from batch_index = torch.where(mask)[0] targeted, batch_index = torch.unique(batch_index, return_inverse=True) for i in range(len(targeted)): outputs[targeted[i]][readout_name] = task_output[batch_index == i] else: outputs[readout_name] = task_output return outputs
[docs] def forward_varlen( self, output_embs: torch.Tensor, output_readout_index: torch.Tensor, output_batch_index: torch.Tensor, unpack_output: bool = False, ) -> dict[str, torch.Tensor] | list[dict[str, torch.Tensor]]: """Project chained output embeddings through their per-task heads. As :meth:`forward`, but for sequences chained along a single batch dimension rather than padded, which avoids the padding memory. Args: output_embs: Output embeddings, ``(total_ntokens, dim)``. output_readout_index: Routing index per output token, ``(total_ntokens,)``. output_batch_index: Batch index per output token, ``(total_ntokens,)``. unpack_output: Return one dict per batch sample. See :meth:`forward`. Returns: See :meth:`forward`. """ n_samples = output_batch_index.max().item() + 1 outputs = [{} for _ in range(n_samples)] if unpack_output else {} for readout_id in output_readout_index.unique().tolist(): readout_name = self._readout_id_to_name.get(readout_id, None) # ids with no head are padding, or tasks excluded from this readout if readout_name is None: continue mask = output_readout_index == readout_id task_output = self.projections[readout_name](output_embs[mask]) if unpack_output: # sequences were chained, so batch membership comes from the index batch_index = output_batch_index[mask] targeted, batch_index = torch.unique(batch_index, return_inverse=True) for i in range(len(targeted)): outputs[targeted[i]][readout_name] = task_output[batch_index == i] else: outputs[readout_name] = task_output return outputs