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)
|