File size: 6,094 Bytes
6f75f79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""M10 -- Mixture-of-Depths (MoD): a per-block token router for fewer FLOPs/token.

External basis: Raposo et al., "Mixture-of-Depths: Dynamically allocating compute in
transformer-based language models" (DeepMind, 2024).

Idea: not every token needs every block. A tiny fp router scores each token; the top
`capacity` fraction (e.g. 0.5) are passed through the wrapped block (attention+FFN);
the remaining tokens BYPASS the block entirely via the residual -- their compute is
skipped. The compute budget per block is therefore fixed at `capacity * tokens`.

So the router learns *which* tokens matter, we multiply the block's contribution for the
selected tokens by the router's sigmoid weight (a weighted residual / straight-through
style). Skipped tokens are returned bit-identically (pure residual identity).

CAUSAL CAVEAT (important): the standard MoD top-k selection is a NON-CAUSAL operation --
it ranks tokens within a fixed window, so token i's keep/skip decision can depend on
later tokens. For training a witness on a fixed block this is fine (and is exactly the
"expert-choice"-style routing DeepMind use during training). At inference / for a strictly
causal model you would instead train a per-token predictor of the routing decision (the
"router predictor" in the paper) so no future information leaks. We do top-k over the
batch*time tokens here for the witness; this is acceptable for measuring the FLOP/quality
tradeoff but is NOT a drop-in causal inference path.

Pure PyTorch, CPU/CUDA.
"""
from __future__ import annotations

import torch
import torch.nn as nn

__all__ = ["MoDBlock"]


class MoDBlock(nn.Module):
    """Wrap a block (any ``nn.Module`` mapping ``(B,T,D) -> (B,T,D)``) with token routing.

    Args:
        block: the inner block to (conditionally) apply. It MUST already be a residual block,
            i.e. ``block(x)`` returns ``x + delta`` (as the TinyGPT / models.py Block does).
            MoD then mixes ``block(x_sel)`` for selected tokens and passes the rest through
            unchanged.
        capacity: fraction in (0, 1] of tokens to route through the block. ``1.0`` means every
            token is processed (exact parity with the unwrapped block, modulo the router weight,
            see ``use_router_weight``).
        use_router_weight: if True (default, the MoD recipe), the block's *contribution* for a
            selected token is scaled by the router's sigmoid weight, so the router gets a
            gradient signal telling it which tokens benefit from the block. At capacity=1.0 this
            is disabled automatically so the wrapper is an exact identity over the block.

    The realized fraction of tokens actually processed is exposed via ``last_processed_fraction``
    after each forward (and via ``realized_fraction()``).
    """

    def __init__(self, block: nn.Module, capacity: float = 0.5,
                 use_router_weight: bool = True) -> None:
        super().__init__()
        if not (0.0 < capacity <= 1.0):
            raise ValueError(f"capacity must be in (0, 1], got {capacity}")
        self.block = block
        self.capacity = float(capacity)
        self.use_router_weight = bool(use_router_weight)
        self._d = self._infer_dim(block)
        # tiny fp router: one score per token. AdamW owns this Parameter.
        self.router = nn.Linear(self._d, 1)
        nn.init.zeros_(self.router.bias)
        nn.init.normal_(self.router.weight, std=0.02)
        self.last_processed_fraction: float = 0.0

    @staticmethod
    def _infer_dim(block: nn.Module) -> int:
        for attr in ("n_embd", "d", "dim"):
            if hasattr(block, attr) and isinstance(getattr(block, attr), int):
                return getattr(block, attr)
        # fall back to the first LayerNorm / Linear feature size we can find
        for m in block.modules():
            if isinstance(m, nn.LayerNorm):
                return m.normalized_shape[0]
            if isinstance(m, nn.Linear):
                return m.in_features
        raise ValueError("could not infer block embedding dim; pass a block exposing .n_embd/.d")

    def realized_fraction(self) -> float:
        """Fraction of tokens actually pushed through the block on the last forward."""
        return self.last_processed_fraction

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        b, t, d = x.shape
        n = b * t
        flat = x.reshape(n, d)

        # capacity=1.0 -> process everything; this is EXACT parity with self.block(x).
        if self.capacity >= 1.0:
            self.last_processed_fraction = 1.0
            return self.block(x)

        k = max(1, int(round(self.capacity * n)))
        k = min(k, n)

        scores = self.router(flat).squeeze(-1)        # (n,)
        # top-k tokens by router score get the block; the rest bypass via the residual.
        top_val, top_idx = torch.topk(scores, k, sorted=False)
        self.last_processed_fraction = k / n

        sel = flat.index_select(0, top_idx).view(1, k, d)   # (1,k,d) keeps block's (B,T,D) API
        block_out = self.block(sel).view(k, d)              # = sel + delta_sel (residual block)

        if self.use_router_weight:
            # weighted residual: scale the block's *delta* by the router's sigmoid weight so the
            # router receives gradient. out = sel + w * (block_out - sel) = sel + w*delta.
            w = torch.sigmoid(top_val).unsqueeze(-1)        # (k,1)
            new_sel = sel.view(k, d) + w * (block_out - sel.view(k, d))
        else:
            new_sel = block_out

        # start from the pure residual (every token passed through unchanged) ...
        out = flat.clone()
        # ... then overwrite the selected rows with the block's (weighted) output.
        out = out.index_copy(0, top_idx, new_sel)
        return out.view(b, t, d)

    def block_flop_fraction(self) -> float:
        """The fraction of the unwrapped block's FLOPs this wrapper spends (== capacity).

        The router adds a tiny d-per-token term on top, negligible vs the block's ~d^2.
        """
        return self.capacity