File size: 11,697 Bytes
10a0ca0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import math
import os
import torch
import torch.nn as nn
from typing import Dict

from ..common import trunc_normal_init_


def shift(input:torch.Tensor, shift, dim=0, fillval=0):
    # torch.roll without the copy of the wrap-around section
    size = input.size(dim)
    fill = torch.full_like(input.narrow(dim, 0, abs(shift)), fillval)
    if shift > 0:
        output = torch.cat([fill, input.narrow(dim, 0, size-shift)], dim=dim)
    if shift < 0:
        output = torch.cat([input.narrow(dim, -shift, size+shift), fill], dim=dim)
    return output

class WindowMix(nn.Module):
    """Learned convex mix of the last m iterate STATES, used as the damping
    anchor inside the fixed-point step. Weights are content-based (query =
    current iterate, keys = window states) plus a learnable per-age recency
    bias, passed through softmax -> convex (sum=1). Values are the RAW states
    (no v/o projection), so the read stays in the convex hull of the window;
    at convergence every slot == y* so the mix returns y* -> the fixed point is
    preserved exactly. recency_init large + q zero-init => starts one-hot on the
    newest state == byte-identical baseline damping. This is learned multi-point
    Krasnoselskii-Mann damping (a generalization of FPRM's fixed 2-point blend),
    NOT Anderson acceleration (which uses non-convex affine weights)."""

    def __init__(self, hidden: int, window: int, recency_init: float = 8.0):
        super().__init__()
        self.q = nn.Linear(hidden, hidden, bias=False)
        self.k = nn.Linear(hidden, hidden, bias=False)
        nn.init.zeros_(self.q.weight)   # content logit == 0 at init -> pure recency
        self.scale = hidden ** -0.5
        self.recency = nn.Parameter(-recency_init * torch.arange(window, dtype=torch.float32))

    def forward(self, window: torch.Tensor) -> torch.Tensor:   # window: [m, B, S, H], index 0 = newest
        m = window.shape[0]
        cur = window[0]
        q = self.q(cur).unsqueeze(0)                                  # [1, B, S, H]
        k = self.k(window)                                            # [m, B, S, H]
        logit = (q * k).sum(-1) * self.scale                          # [m, B, S]
        logit = logit + self.recency.to(logit.dtype).view(m, 1, 1)    # + learned age bias
        w = torch.softmax(logit, dim=0)                               # [m, B, S] convex
        return (w.unsqueeze(-1) * window).sum(dim=0).to(window.dtype)  # [B, S, H]


class FixedPointOptimizer(nn.Module):
    def __init__(self, config: dict):
        super().__init__()

        self.stepsize = config.stepsize
        self.stepsize_decay = config.stepsize_decay
        self.decay_patience = config.decay_patience
        self.eps = config.eps
        self.outlier_quantile = config.outlier_quantile
        self.max_iter = config.max_iter
        self.init_std = config.init_std
        self.additive_noise_std = config.additive_noise_std
        self.fp_thresh = config.fp_thresh

        # Multi-point convex damping on the loop axis. Window size from config
        # (learned mixer, for training) or env (zero-training probe). m<=1 ->
        # byte-identical baseline. See loop-attn plan.
        cfg_window = int(getattr(config, "loop_window", 0))
        env_learned = int(os.environ.get("FP_LEARNED_WINDOW", "0"))
        env_uniform = int(os.environ.get("FP_UNIFORM_WINDOW", "0"))
        self.loop_window = cfg_window or env_learned or env_uniform
        self.window_recency = float(os.environ.get("FP_WINDOW_RECENCY", "0"))  # only used by the env probe path
        learned = (cfg_window > 1) or (env_learned > 1)
        if learned and self.loop_window > 1:
            self.window_mix = WindowMix(config.hidden_size, self.loop_window,
                                        float(getattr(config, "loop_recency_init", 8.0)))
        else:
            self.window_mix = None

        self.fixed_init = config.fixed_init
        if self.fixed_init:
            fwd_dtype = getattr(torch, config.forward_dtype)
            self.init_vec = nn.Buffer(
                trunc_normal_init_(torch.empty(config.hidden_size, dtype=fwd_dtype), std=self.init_std),
                persistent=True,
            )

    def detach_state(self, state: dict):
        for k, v in state.items():
            if torch.is_tensor(v):
                state[k] = v.detach()
        return state
        
    def reset(self, reset_flag: torch.Tensor, shape: tuple, dtype: torch.dtype, 
              device: torch.device, state: dict, reset_metadata: bool = False):
        batch_size, seq_len, hidden_size = shape[0], shape[1], shape[2]
        reset_flag_1d = reset_flag.view(-1)
        reset_flag_3d = reset_flag_1d.view(-1, 1, 1)
        old_hist = state.get('hist') if state is not None else None

        if self.fixed_init:
            y = self.init_vec.to(dtype=dtype, device=device).expand(batch_size, seq_len, hidden_size).contiguous()
        else:
            y = trunc_normal_init_(torch.empty(batch_size, seq_len, hidden_size, dtype=dtype, device=device), std=self.init_std)
        residues = torch.inf * torch.ones(batch_size).to(device)
        stepsize = (self.stepsize * torch.ones(batch_size, 1, 1, dtype=dtype, device=device))
        patience = self.decay_patience * torch.ones(batch_size).to(device)
        iter_idx = torch.zeros(batch_size, dtype=torch.int32, device=device)
        best_residues = torch.inf * torch.ones(batch_size).to(device)

        if state is None:
            state = dict(y=y.contiguous(),
                         residues=residues,
                         stepsize=stepsize,
                         patience=patience,
                         iter_idx=iter_idx,
                         best_residues=best_residues)
        else:
            state = dict(y=torch.where(reset_flag_3d, y.contiguous(), state['y']),
                         residues=residues if reset_metadata else torch.where(reset_flag_1d, residues, state['residues']),
                         stepsize=stepsize if reset_metadata else torch.where(reset_flag_3d, stepsize, state['stepsize']),
                         patience=patience if reset_metadata else torch.where(reset_flag_1d, patience, state['patience']),
                         iter_idx=iter_idx if reset_metadata else torch.where(reset_flag_1d, iter_idx, state['iter_idx']),
                         best_residues=best_residues if reset_metadata else torch.where(reset_flag_1d, best_residues, state['best_residues']))

        if self.loop_window > 1:
            m = self.loop_window
            hist_new = y.detach().unsqueeze(0).repeat(m - 1, 1, 1, 1).contiguous()   # [m-1, B, S, H]
            if old_hist is None:
                state['hist'] = hist_new
            else:
                state['hist'] = torch.where(reset_flag_3d.unsqueeze(0), hist_new, old_hist)

        return state

    def step(self, state:Dict[str, torch.Tensor], y:torch.Tensor):
        state_dtype = state["y"].dtype
        if y.dtype != state_dtype:
            y = y.to(state_dtype)

        with torch.no_grad():
            residues = (state['y'].detach() - y.detach()).norm(p=torch.inf, dim=-1) / (y.detach().norm(p=torch.inf, dim=-1) + self.eps)
            residues = residues.max(dim=1)[0]

        stepsize = state['stepsize']
        if stepsize.dtype != state_dtype:
            stepsize = stepsize.to(state_dtype)
        if self.loop_window > 1 and 'hist' in state:
            prev_y = state['y']
            window = torch.cat([prev_y.unsqueeze(0), state['hist']], dim=0)          # [m, B, S, H]  (index 0 = newest)
            if self.window_mix is not None:
                blend_anchor = self.window_mix(window)                               # learned convex mix
            else:
                m = window.shape[0]
                ages = torch.arange(m, device=window.device, dtype=window.dtype)     # 0=newest .. m-1=oldest
                w = torch.softmax(-self.window_recency * ages, dim=0)                # env probe: fixed convex weights
                blend_anchor = (w.view(m, 1, 1, 1) * window).sum(dim=0)
            state['y'] = y * stepsize + blend_anchor * (1 - stepsize) + self.additive_noise_std * torch.randn_like(y)
            state['hist'] = torch.cat([prev_y.unsqueeze(0), state['hist'][:-1]], dim=0)  # roll: newest first, drop oldest
        else:
            state['y'] = y * stepsize + state['y'] * (1 - stepsize) + self.additive_noise_std * torch.randn_like(state['y'])

        # Track per-sample best iterate; require improvement of at least 0.01
        # so slow monotonic drift doesn't keep resetting patience.
        improved = residues < state['best_residues'] - 1e-2

        # update patience and lowest residue
        state['residues'] = residues
        state['best_residues'] = torch.where(improved, residues, state['best_residues'])
        state['patience'] = torch.where(improved, self.decay_patience, state['patience']-1)

        # update damping factor, reset patience
        adapt = (state['patience'] <= 0) & (state['residues'] >= self.fp_thresh)
        state['patience'] = torch.where(adapt, self.decay_patience, state['patience'])
        stepsize_dtype = state['stepsize'].dtype
        # if not self.training:
        state['stepsize'] = state['stepsize'] * torch.where(adapt, self.stepsize_decay, 1).to(stepsize_dtype).reshape(-1, 1, 1)

        state['iter_idx'] = state['iter_idx'] + 1
        return state

    def cont(self, state: Dict[str, torch.Tensor], thresh: float):
        if int(state['iter_idx'].max().item()) == 0:
            return self.max_iter > 0

        q = 1 - self.outlier_quantile if self.training else 1.0
        return (
            (torch.quantile(state['residues'].float(), q=q) >= thresh)
            & (torch.quantile(state['iter_idx'].float(), q=q) < self.max_iter)
            & (torch.quantile(state['stepsize'].float(), q=q) > 1e-3)
        )

class VariationalDropout(nn.Module):
    def __init__(self, dropout: float = 0.0):
        super().__init__()
        assert 0.0 <= dropout < 1.0, f"dropout must be in [0, 1), got {dropout}"
        self.dropout = dropout
        self._mask: torch.Tensor | None = None

    def sample_mask(self, x: torch.Tensor) -> None:
        if not self.training or self.dropout == 0.0:
            self._mask = None
            return
        keep_prob = 1.0 - self.dropout
        self._mask = torch.bernoulli(torch.full_like(x, keep_prob)) / keep_prob

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        if not self.training or self.dropout == 0.0 or self._mask is None:
            return x
        return x * self._mask


class VariationalDropToken1d(nn.Module):
    def __init__(self, dropout: float = 0.0, token_first: bool = True):
        super().__init__()
        assert 0.0 <= dropout < 1.0, f"dropout must be in [0, 1), got {dropout}"
        self.dropout = dropout
        self.token_first = token_first
        self._mask: torch.Tensor | None = None

    def sample_mask(self, x: torch.Tensor) -> None:
        if not self.training or self.dropout == 0.0:
            self._mask = None
            return
        keep_prob = 1.0 - self.dropout
        if self.token_first:
            B, L, _ = x.shape
            self._mask = torch.bernoulli(torch.full((B, L, 1), keep_prob, device=x.device, dtype=x.dtype)) / keep_prob
        else:
            B, _, L = x.shape
            self._mask = torch.bernoulli(torch.full((B, 1, L), keep_prob, device=x.device, dtype=x.dtype)) / keep_prob

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        if not self.training or self.dropout == 0.0 or self._mask is None:
            return x
        return x * self._mask