File size: 17,969 Bytes
18ffde9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
"""
Cammino vettorizzato sulla mesh per CascadeMeshModel / V1OnlyMeshModel.

Sostituisce il loop Python per faccia di ``CascadeMeshModel.walk_token``
(``for f_id in face[active].unique().tolist()`` + ``forward_face`` con
``.item()`` e maschere booleane) con un dispatch raggruppato alla MegaBlocks:

  - i parametri dei TriangleBlock sono impilati per face id (stack
    differenziabile dei parametri esistenti: state_dict e checkpoint invariati);
  - i token attivi sono disposti per faccia (posizioni da un cumsum, niente
    sort): le facce con pochi token in un elemento di batch per faccia, quelle
    affollate in blocchi da ``block_rows`` righe con i pesi raccolti per blocco
    (vedi ``grouped_block_forward`` per il perché dei percorsi);
  - ogni Linear del blocco diventa un ``baddbmm`` per percorso;
  - le uscite tornano ai token con ``index_select`` e ``torch.where``.

Le forme dipendono solo dal numero di token B, mai dai dati: nel salto non ci
sono ``unique``, ``tolist``, ``nonzero`` né ``.item()``.

Puro refactoring: proiezione sulla faccia, router, Gumbel-ST, soglia di uscita
e peso straight-through sull'uscita intera del blocco sono quelli del modello
(si chiamano gli stessi metodi ``project_to_face``, ``_route_step``,
``_scatter_side_oh`` nello stesso ordine), quindi anche il consumo del
generatore casuale è lo stesso.  Restano due sincronizzazioni, entrambe
necessarie per l'equivalenza:
  - ``active.any()`` a inizio salto con routing soft: il codice originale esce
    dal ciclo quando tutti i token sono usciti e da lì non estrae più rumore
    Gumbel (in routing hard i salti in più sono no-op e il controllo è saltato);
  - nel backward, una lettura per banca della maschera delle facce visitate:
    le facce mai visitate devono restare con grad None come nel loop.

``fast_walk_token_v6`` è il cammino del router v6 (vedi geodesic_stages_model):
stesso dispatch raggruppato, peso straight-through passato come ``gate`` sul
solo ramo dell'esperto.
"""
from __future__ import annotations

from typing import Dict, List, Optional, Sequence, Tuple

import torch
import torch.nn as nn
import torch.nn.functional as F

DEFAULT_BLOCK_ROWS = 16  # >= 16: regime "M grande" del Linear su MPS
DEFAULT_DENSE_ROWS = 15  # <= 15: regime "M piccolo" del Linear su MPS

_NAMES = ("ln_w", "ln_b", "w1", "b1", "w2", "b2", "wr", "br")


def _check_block(blk: nn.Module) -> None:
    norm, ff, router = blk.norm, blk.ff, blk.router
    ok = (
        isinstance(norm, nn.LayerNorm)
        and norm.weight is not None
        and norm.bias is not None
        and len(norm.normalized_shape) == 1
        and isinstance(ff, nn.Sequential)
        and len(ff) == 3
        and isinstance(ff[0], nn.Linear)
        and isinstance(ff[1], nn.GELU)
        and ff[1].approximate == "none"
        and isinstance(ff[2], nn.Linear)
        and ff[0].bias is not None
        and ff[2].bias is not None
        and isinstance(router, nn.Linear)
        and router.bias is not None
    )
    if not ok:
        raise TypeError("fast_walk: TriangleBlock con struttura inattesa (LN → Linear → GELU → Linear, router)")


def _face_blocks(bank: nn.Module) -> List[nn.Module]:
    """TriangleBlock in ordine di face id (il ModuleList è in ordine di packing a coppie)."""
    slots = bank.__dict__.get("_fast_walk_slots")
    if slots is None:
        slots = [int(s) for s in bank.face_to_slot.detach().cpu().tolist()]
        if sorted(slots) != list(range(len(bank.blocks))) or len(slots) != int(bank.num_faces):
            raise ValueError("fast_walk: face_to_slot non è una permutazione dei blocchi")
        for s in slots:
            _check_block(bank.blocks[s])
        bank.__dict__["_fast_walk_slots"] = slots
    return [bank.blocks[s] for s in slots]


def _block_params(blk: nn.Module) -> Tuple[torch.Tensor, ...]:
    return (
        blk.norm.weight,
        blk.norm.bias,
        blk.ff[0].weight,
        blk.ff[0].bias,
        blk.ff[2].weight,
        blk.ff[2].bias,
        blk.router.weight,
        blk.router.bias,
    )


class _VisitedFaces:
    """Facce che hanno eseguito il blocco in almeno un salto (maschera su device, letta nel backward)."""

    def __init__(self) -> None:
        self.mask: Optional[torch.Tensor] = None
        self._host: Optional[List[bool]] = None

    def add(self, counts: torch.Tensor) -> None:
        hit = counts > 0
        self.mask = hit if self.mask is None else (self.mask | hit)

    def host(self, n_faces: int) -> List[bool]:
        if self._host is None:
            self._host = [False] * n_faces if self.mask is None else [bool(v) for v in self.mask.tolist()]
        return self._host


class _StackFaces(torch.autograd.Function):
    """torch.stack per tipo di parametro; nel backward le facce mai visitate ricevono None.

    Nel loop originale i parametri di una faccia non visitata non entrano nel
    grafo e restano con grad None (AdamW li salta: niente momento né weight
    decay); un grad di zeri cambierebbe l'aggiornamento. Per lo stesso motivo
    un tipo di parametro fuori dal grafo differenziabile (il router con routing
    hard) deve restare None: niente gradienti materializzati a zero.
    """

    @staticmethod
    def forward(ctx, visited: _VisitedFaces, n_faces: int, *flat: torch.Tensor):
        ctx.set_materialize_grads(False)
        ctx.visited = visited
        ctx.n_faces = n_faces
        return tuple(torch.stack(flat[k : k + n_faces]) for k in range(0, len(flat), n_faces))

    @staticmethod
    def backward(ctx, *grads):
        hit = ctx.visited.host(ctx.n_faces)
        out: List[Optional[torch.Tensor]] = [None, None]
        for g in grads:
            if g is None:
                out.extend([None] * ctx.n_faces)
            else:
                out.extend(gf if h else None for gf, h in zip(g.unbind(0), hit))
        return tuple(out)


def stack_bank(bank: nn.Module) -> Dict[str, object]:
    """Parametri della banca impilati per face id: ln (F,d), w1 (F,h,d), w2 (F,d,h), router (F,4,d).

    Con autograd attivo lo stack è nel grafo (i gradienti tornano ai parametri
    per faccia) e ``st["visited"]`` va aggiornato dal cammino. Senza gradienti
    il risultato resta in cache finché i parametri non cambiano (data_ptr e
    contatore di versione).
    """
    blocks = _face_blocks(bank)
    params = [_block_params(b) for b in blocks]
    n_faces = len(blocks)
    grad_mode = torch.is_grad_enabled() and any(p.requires_grad for ps in params for p in ps)
    if grad_mode:
        visited = _VisitedFaces()
        flat = [ps[i] for i in range(len(_NAMES)) for ps in params]
        st: Dict[str, object] = dict(zip(_NAMES, _StackFaces.apply(visited, n_faces, *flat)))
        st["visited"] = visited
        bank.__dict__.pop("_fast_walk_cache", None)
    else:
        key = (torch.is_inference_mode_enabled(),) + tuple(
            (p.data_ptr(), p._version, p.device, p.dtype) for ps in params for p in ps
        )
        cached = bank.__dict__.get("_fast_walk_cache")
        if cached is not None and cached[0] == key:
            return cached[1]
        st = {n: torch.stack([ps[i] for ps in params]) for i, n in enumerate(_NAMES)}
        st["visited"] = None
        bank.__dict__["_fast_walk_cache"] = (key, st)
    st["eps"] = float(blocks[0].norm.eps)
    st["num_faces"] = n_faces
    return st


def _triangle(
    xb: torch.Tensor, w: Sequence[torch.Tensor], eps: float, gate: Optional[torch.Tensor] = None
) -> Tuple[torch.Tensor, torch.Tensor]:
    """TriangleBlock.forward a gruppi: xb (G, R, d), pesi (G, ...) di un blocco per gruppo.

    gate (G, R, 1), router v6: xb + gate · ff(norm(xb)), il peso non tocca il residuo.
    """
    ln_w, ln_b, w1, b1, w2, b2, wr, br = w
    d = xb.shape[-1]
    xn = F.layer_norm(xb, (d,), None, None, eps) * ln_w.unsqueeze(1) + ln_b.unsqueeze(1)
    hid = F.gelu(torch.baddbmm(b1.unsqueeze(1), xn, w1.transpose(1, 2)))
    delta = torch.baddbmm(b2.unsqueeze(1), hid, w2.transpose(1, 2))
    xo = xb + delta if gate is None else xb + gate * delta
    return xo, torch.baddbmm(br.unsqueeze(1), xo, wr.transpose(1, 2))


def _scatter_rows(x: torch.Tensor, dest: torch.Tensor, rows: int) -> torch.Tensor:
    """Buffer (rows, d) con x[i] alla riga dest[i]; dest == rows è la riga di scarto."""
    return x.new_zeros(rows + 1, x.shape[1]).index_copy(0, dest, x)[:rows]


def grouped_block_forward(
    st: Dict[str, object],
    x: torch.Tensor,
    face: torch.Tensor,
    active: torch.Tensor,
    block_rows: int = DEFAULT_BLOCK_ROWS,
    dense_rows: int = DEFAULT_DENSE_ROWS,
    gate: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
    """TriangleBlock della propria faccia su ogni token attivo, senza sincronizzazioni.

    x (B, d), face (B,) long, active (B,) bool, gate (B, 1) opzionale (router v6:
    peso straight-through sul solo ramo dell'esperto, disposto come x).
    Ritorna (x_out, logits, counts): (B, d), (B, 4) e token attivi per faccia (F,).
    Sulle righe inattive x_out/logits sono finiti ma arbitrari (il chiamante li
    scarta con torch.where).

    Nel loop originale ogni faccia fa i suoi Linear su tutte e sole le sue n_f
    righe, e il kernel dipende da n_f: su MPS uno per n_f <= 15 e uno per
    n_f >= 16, su CPU (BLAS) uno per n_f == 1 e uno per n_f >= 2; dentro lo
    stesso regime le righe sono bit-identiche qualunque sia n_f. Per restare
    bit-identici ci sono tre percorsi a forma fissa, ognuno nel regime giusto:
      - facce con n_f <= dense_rows (da 2 su CPU): un elemento di batch per
        faccia con dense_rows righe, sui pesi impilati senza gather;
      - su CPU, facce con un solo token: un elemento da 1 riga per faccia;
      - facce con n_f > dense_rows: blocchi da block_rows righe (padding solo
        nell'ultimo blocco di ogni faccia) con i pesi raccolti per blocco.
    """
    b, d = x.shape
    n_faces = int(st["num_faces"])
    s, c = int(block_rows), int(dense_rows)
    weights = [st[n] for n in _NAMES]  # (F, ...)
    eps = float(st["eps"])  # type: ignore[arg-type]
    dev = x.device
    single = dev.type == "cpu"
    lo = 2 if single else 1

    key = torch.where(active, face, torch.full_like(face, n_faces))  # inattivi → classe fittizia F
    face_c = key.clamp(max=n_faces - 1)
    onehot = (key.unsqueeze(1) == torch.arange(n_faces + 1, device=dev)).to(torch.int64)
    rank = onehot.cumsum(0).gather(1, key.unsqueeze(1)).squeeze(1) - 1  # posizione nella faccia
    counts = onehot[:, :n_faces].sum(0)
    n_tok = counts.gather(0, face_c)
    in_big = active & (n_tok > c)
    in_dense = active & (n_tok >= lo) & ~in_big
    zero = torch.zeros_like(rank)
    parts: List[Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]] = []  # (maschera, x_out, logit, riga)

    def _gate(dest: torch.Tensor, rows: int, shape: Tuple[int, int]) -> Optional[torch.Tensor]:
        return None if gate is None else _scatter_rows(gate, dest, rows).view(*shape, 1)

    if c >= lo:
        dest = torch.where(in_dense, face * c + rank, torch.full_like(rank, n_faces * c))
        xo, lg = _triangle(
            _scatter_rows(x, dest, n_faces * c).view(n_faces, c, d), weights, eps, _gate(dest, n_faces * c, (n_faces, c))
        )
        parts.append((in_dense, xo.reshape(-1, d), lg.reshape(n_faces * c, -1), torch.where(in_dense, dest, zero)))
    if single:
        alone = active & (n_tok == 1)
        dest = torch.where(alone, face, torch.full_like(face, n_faces))
        xo, lg = _triangle(
            _scatter_rows(x, dest, n_faces).unsqueeze(1), weights, eps, _gate(dest, n_faces, (n_faces, 1))
        )
        parts.append((alone, xo.reshape(-1, d), lg.reshape(n_faces, -1), face_c))

    # Limite statico: sum_f ceil(n_f / s) <= ceil(B / s) + facce con n_f > c.
    n_blocks = -(-b // s) + min(n_faces, b // (c + 1))
    rows = n_blocks * s
    nblk = torch.div(torch.where(counts > c, counts, 0) + (s - 1), s, rounding_mode="floor")
    blk_end = nblk.cumsum(0)
    blk_start = blk_end - nblk
    dest = torch.where(in_big, blk_start.gather(0, face_c) * s + rank, torch.full_like(rank, rows))
    blk_face = (blk_end.unsqueeze(0) <= torch.arange(n_blocks, device=dev).unsqueeze(1)).sum(1)
    blk_face = blk_face.clamp(max=n_faces - 1)  # blocchi oltre il totale: righe a zero, uscite ignorate
    gathered = [w.index_select(0, blk_face) for w in weights]
    xo, lg = _triangle(_scatter_rows(x, dest, rows).view(n_blocks, s, d), gathered, eps, _gate(dest, rows, (n_blocks, s)))
    row = torch.where(in_big, dest, zero)
    out = xo.reshape(rows, d).index_select(0, row)
    logits = lg.reshape(rows, -1).index_select(0, row)

    for mask, p_out, p_lg, p_row in parts:
        m = mask.unsqueeze(-1)
        out = torch.where(m, p_out.index_select(0, p_row), out)
        logits = torch.where(m, p_lg.index_select(0, p_row), logits)
    return out, logits, counts


def _build_paths(face0: torch.Tensor, codes: Sequence[torch.Tensor]) -> List[List[int]]:
    start = face0.tolist()
    if not codes:
        return [[int(f)] for f in start]
    table = torch.stack(list(codes), dim=1).tolist()
    return [[int(f)] + [c for c in row if c != -2] for f, row in zip(start, table)]


def fast_walk_token(
    model: nn.Module,
    h: torch.Tensor,
    bank: nn.Module,
    to_sphere: nn.Linear,
    record_path: bool = False,
    block_rows: Optional[int] = None,
    dense_rows: Optional[int] = None,
) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
    """Stessa semantica di CascadeMeshModel.walk_token (loop per faccia), vettorizzata."""
    b = h.shape[0]
    face, face_oh = model.project_to_face(h, to_sphere)
    face0 = face
    max_hops = int(model.walk.max_hops)
    if b == 0 or max_hops <= 0:
        return h, (_build_paths(face0, []) if record_path else None)

    st = stack_bank(bank)
    rows = int(block_rows or getattr(model, "fast_walk_block_rows", 0) or DEFAULT_BLOCK_ROWS)
    dense = dense_rows if dense_rows is not None else getattr(model, "fast_walk_dense_rows", None)
    dense = DEFAULT_DENSE_ROWS if dense is None else int(dense)
    soft = not model._hard_routing()
    active = torch.ones(b, dtype=torch.bool, device=h.device)
    codes: List[torch.Tensor] = []

    for hop in range(max_hops):
        if hop > 0 and soft and not bool(active.any()):
            break
        out, lg, counts = grouped_block_forward(st, h, face, active, rows, dense)
        if st["visited"] is not None:
            st["visited"].add(counts)  # type: ignore[union-attr]
        act = active.unsqueeze(-1)
        if face_oh is not None:
            # ST: w≈1 in forward; backward → face_oh → to_sphere / router
            w = face_oh.gather(1, face.unsqueeze(1))
            h = torch.where(act, out * w, h)
        else:
            h = torch.where(act, out, h)
        logits = torch.where(act, lg, torch.zeros_like(lg))
        next_face, exit_mask, side_oh = model._route_step(face, logits)
        moved = active & ~exit_mask
        if side_oh is not None:
            scale = side_oh.sum(dim=-1, keepdim=True)
            h = torch.where(act, h * scale, h)
            routed_oh = model._scatter_side_oh(face, side_oh)
            if face_oh is None:
                face_oh = routed_oh
            else:
                face_oh = torch.where(moved.unsqueeze(-1), routed_oh, face_oh)
        face = torch.where(moved, next_face, face)
        just_exit = active & exit_mask
        active = active & ~exit_mask
        if record_path:
            codes.append(torch.where(moved, face, torch.where(just_exit, -1, -2)))

    return h, (_build_paths(face0, codes) if record_path else None)


def fast_walk_token_v6(
    model: nn.Module,
    h: torch.Tensor,
    bank: nn.Module,
    to_sphere: Optional[nn.Linear],
    entry_ids: Optional[torch.Tensor] = None,
    record_path: bool = False,
    block_rows: Optional[int] = None,
    dense_rows: Optional[int] = None,
    active: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
    """Cammino del router v6 / hash, vettorizzato: semantica di ``CascadeMeshModel._walk_token_v6_loop``.

    Nessuna sincronizzazione nel salto: senza l'uscita anticipata su
    ``active.any()`` i salti dopo che tutti i token sono usciti sono no-op.
    ``active`` opzionale (B,): token inattivi (pad/unk stadi 2/3) saltano le FFN;
    in ``record_path`` la faccia d'ingresso è ``-1``.
    """
    b = h.shape[0]
    face, w = model._v6_entry(h, bank, to_sphere, entry_ids)
    max_hops = int(model.walk.max_hops)
    if active is None:
        active = torch.ones(b, dtype=torch.bool, device=h.device)
    else:
        active = active.to(device=h.device, dtype=torch.bool)
    face0 = torch.where(active, face, torch.full_like(face, -1))
    if b == 0 or max_hops <= 0:
        return h, (_build_paths(face0, []) if record_path else None)

    st = stack_bank(bank)
    rows = int(block_rows or getattr(model, "fast_walk_block_rows", 0) or DEFAULT_BLOCK_ROWS)
    dense = dense_rows if dense_rows is not None else getattr(model, "fast_walk_dense_rows", None)
    dense = DEFAULT_DENSE_ROWS if dense is None else int(dense)
    codes: List[torch.Tensor] = []

    for _ in range(max_hops):
        out, lg, counts = grouped_block_forward(st, h, face, active, rows, dense, gate=w)
        if st["visited"] is not None:
            st["visited"].add(counts)  # type: ignore[union-attr]
        act = active.unsqueeze(-1)
        h = torch.where(act, out, h)
        logits = torch.where(act, lg, torch.zeros_like(lg))
        next_face, exit_mask, w = model._v6_route(face, logits)
        moved = active & ~exit_mask
        face = torch.where(moved, next_face, face)
        just_exit = active & exit_mask
        active = active & ~exit_mask
        if record_path:
            codes.append(torch.where(moved, face, torch.where(just_exit, -1, -2)))

    return h, (_build_paths(face0, codes) if record_path else None)