Skip to content

Commit 85af0b5

Browse files
Prorotype support for cross_expert_attention_type supporting entmax15 and sparsemax for GLN MoE experts.
1 parent 606b3ed commit 85af0b5

3 files changed

Lines changed: 207 additions & 8 deletions

File tree

src/eir/models/input/array/models_locally_connected.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
from torch import nn
1616

1717
from eir.models.input.sequence.transformer_models import PositionalEmbedding
18-
from eir.models.layers.attention_layers import LinearAttention
18+
from eir.models.layers.attention_layers import (
19+
EntmaxMultiheadAttention,
20+
LinearAttention,
21+
SparseAttentionType,
22+
)
1923
from eir.models.layers.lcl_layers import LCL, LCLResidualBlock
2024
from eir.models.layers.norm_layers import LayerScale
2125
from eir.utils.logging import get_logger
@@ -319,6 +323,13 @@ class LCLInformedMoEModelConfig(LCLModelConfig):
319323
320324
:param cross_expert_attention_dropout:
321325
Dropout probability for cross-expert attention and FFN layers.
326+
327+
:param cross_expert_attention_type:
328+
Attention function used in cross-expert attention blocks.
329+
``"softmax"`` uses standard dense attention.
330+
``"entmax15"`` uses entmax with alpha=1.5, producing sparse attention
331+
weights where some experts receive exactly zero weight.
332+
``"sparsemax"`` uses alpha=2.0, producing even sparser attention.
322333
"""
323334

324335
stub_experts: bool = False
@@ -327,6 +338,7 @@ class LCLInformedMoEModelConfig(LCLModelConfig):
327338
cross_expert_attention_heads: int | None = None
328339
cross_expert_attention_layers: int = 1
329340
cross_expert_attention_dropout: float = 0.10
341+
cross_expert_attention_type: SparseAttentionType = "softmax"
330342

331343

332344
class SwiGLUFFN(nn.Module):
@@ -352,21 +364,38 @@ def __init__(
352364
dim: int,
353365
num_heads: int,
354366
dropout_p: float = 0.0,
367+
attention_type: SparseAttentionType = "softmax",
355368
):
356369
super().__init__()
357370
self.attn_norm = nn.RMSNorm(dim)
358-
self.attn = nn.MultiheadAttention(
359-
embed_dim=dim,
360-
num_heads=num_heads,
361-
dropout=dropout_p,
362-
batch_first=True,
363-
)
371+
372+
if attention_type == "softmax":
373+
self.attn: nn.MultiheadAttention | EntmaxMultiheadAttention = (
374+
nn.MultiheadAttention(
375+
embed_dim=dim,
376+
num_heads=num_heads,
377+
dropout=dropout_p,
378+
batch_first=True,
379+
)
380+
)
381+
else:
382+
self.attn = EntmaxMultiheadAttention(
383+
embed_dim=dim,
384+
num_heads=num_heads,
385+
dropout_p=dropout_p,
386+
attention_type=attention_type,
387+
)
388+
364389
self.ffn_norm = nn.RMSNorm(dim)
365390
self.ffn = SwiGLUFFN(dim=dim, dropout_p=dropout_p)
391+
self._attention_type = attention_type
366392

367393
def forward(self, x: torch.Tensor) -> torch.Tensor:
368394
h = self.attn_norm(x)
369-
h, _ = self.attn(query=h, key=h, value=h, need_weights=False)
395+
if self._attention_type == "softmax":
396+
h, _ = self.attn(query=h, key=h, value=h, need_weights=False)
397+
else:
398+
h = self.attn(x=h)
370399
x = x + h
371400
x = x + self.ffn(self.ffn_norm(x))
372401
return x
@@ -379,6 +408,7 @@ def __init__(
379408
num_heads: int,
380409
num_layers: int = 1,
381410
dropout_p: float = 0.0,
411+
attention_type: SparseAttentionType = "softmax",
382412
):
383413
super().__init__()
384414
self.blocks = nn.ModuleList(
@@ -387,6 +417,7 @@ def __init__(
387417
dim=dim,
388418
num_heads=num_heads,
389419
dropout_p=dropout_p,
420+
attention_type=attention_type,
390421
)
391422
for _ in range(num_layers)
392423
]
@@ -560,6 +591,7 @@ def __init__(
560591
num_heads=model_config.cross_expert_attention_heads,
561592
num_layers=model_config.cross_expert_attention_layers,
562593
dropout_p=model_config.cross_expert_attention_dropout,
594+
attention_type=model_config.cross_expert_attention_type,
563595
)
564596

565597
self._init_weights()

src/eir/models/layers/attention_layers.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from dataclasses import dataclass
2+
from functools import partial
3+
from typing import Literal
24

35
import torch
46
import torch.nn.functional as F
@@ -7,6 +9,164 @@
79

810
from eir.models.layers.norm_layers import LayerScale, RMSNorm
911

12+
SparseAttentionType = Literal["softmax", "entmax15", "sparsemax"]
13+
14+
15+
def _make_ix_like(input: Tensor, dim: int = -1) -> Tensor:
16+
d = input.size(dim)
17+
rho = torch.arange(start=1, end=d + 1, device=input.device, dtype=input.dtype)
18+
view = [1] * input.dim()
19+
view[dim] = -1
20+
return rho.view(view)
21+
22+
23+
class _Entmax15Function(torch.autograd.Function):
24+
"""
25+
All entmax/sparsemax functionality adapted from:
26+
https://github.com/deep-spin/entmax/
27+
"""
28+
29+
@staticmethod
30+
def forward(
31+
ctx: torch.autograd.function.FunctionCtx, input: Tensor, dim: int = -1
32+
) -> Tensor: # type: ignore[override]
33+
ctx.dim = dim # type: ignore[attr-defined]
34+
35+
max_val, _ = input.max(dim=dim, keepdim=True)
36+
input = input - max_val
37+
input = input / 2
38+
39+
x_srt, _ = torch.sort(input=input, descending=True, dim=dim)
40+
rho = _make_ix_like(input=input, dim=dim)
41+
mean = x_srt.cumsum(dim=dim) / rho
42+
mean_sq = (x_srt**2).cumsum(dim=dim) / rho
43+
ss = rho * (mean_sq - mean**2)
44+
delta = (1 - ss) / rho
45+
delta_nz = torch.clamp(input=delta, min=0)
46+
tau = mean - torch.sqrt(delta_nz)
47+
48+
support_size = (tau <= x_srt).sum(dim=dim).unsqueeze(dim=dim)
49+
tau_star = tau.gather(dim=dim, index=support_size - 1)
50+
51+
output = torch.clamp(input=input - tau_star, min=0) ** 2
52+
ctx.save_for_backward(output)
53+
return output
54+
55+
@staticmethod
56+
def backward(
57+
ctx: torch.autograd.function.FunctionCtx, grad_output: Tensor
58+
) -> tuple[Tensor, None]: # type: ignore[override]
59+
(y,) = ctx.saved_tensors # type: ignore[attr-defined]
60+
dim = ctx.dim # type: ignore[attr-defined]
61+
62+
gppr = y.sqrt()
63+
dx = grad_output * gppr
64+
q = dx.sum(dim=dim, keepdim=True) / gppr.sum(dim=dim, keepdim=True).clamp(
65+
min=1e-12
66+
)
67+
dx -= q * gppr
68+
dx *= (y > 0).float()
69+
70+
return dx, None
71+
72+
73+
class _SparsemaxFunction(torch.autograd.Function):
74+
@staticmethod
75+
def forward(
76+
ctx: torch.autograd.function.FunctionCtx, input: Tensor, dim: int = -1
77+
) -> Tensor: # type: ignore[override]
78+
ctx.dim = dim # type: ignore[attr-defined]
79+
80+
max_val, _ = input.max(dim=dim, keepdim=True)
81+
input = input - max_val
82+
83+
z_sorted, _ = torch.sort(input=input, descending=True, dim=dim)
84+
z_cumsum = z_sorted.cumsum(dim=dim)
85+
k = _make_ix_like(input=input, dim=dim)
86+
support = (1 + k * z_sorted > z_cumsum).float()
87+
k_star = support.sum(dim=dim, keepdim=True)
88+
tau = (z_cumsum.gather(dim=dim, index=(k_star - 1).long()) - 1) / k_star
89+
90+
output = torch.clamp(input=input - tau, min=0)
91+
ctx.save_for_backward(output)
92+
return output
93+
94+
@staticmethod
95+
def backward(
96+
ctx: torch.autograd.function.FunctionCtx, grad_output: Tensor
97+
) -> tuple[Tensor, None]: # type: ignore[override]
98+
(output,) = ctx.saved_tensors # type: ignore[attr-defined]
99+
dim = ctx.dim # type: ignore[attr-defined]
100+
101+
nonzero = (output > 0).float()
102+
grad_input = grad_output * nonzero
103+
v_hat = grad_input.sum(dim=dim, keepdim=True) / nonzero.sum(
104+
dim=dim, keepdim=True
105+
).clamp(min=1e-12)
106+
grad_input -= v_hat * nonzero
107+
return grad_input, None
108+
109+
110+
def entmax15(input: Tensor, dim: int = -1) -> Tensor:
111+
return _Entmax15Function.apply(input, dim)
112+
113+
114+
def sparsemax(input: Tensor, dim: int = -1) -> Tensor:
115+
return _SparsemaxFunction.apply(input, dim)
116+
117+
118+
class EntmaxMultiheadAttention(nn.Module):
119+
def __init__(
120+
self,
121+
embed_dim: int,
122+
num_heads: int,
123+
dropout_p: float = 0.0,
124+
attention_type: SparseAttentionType = "entmax15",
125+
):
126+
super().__init__()
127+
assert embed_dim % num_heads == 0
128+
129+
self.embed_dim = embed_dim
130+
self.num_heads = num_heads
131+
self.head_dim = embed_dim // num_heads
132+
self.scale = self.head_dim**-0.5
133+
self.attention_type = attention_type
134+
135+
self.q_proj = nn.Linear(
136+
in_features=embed_dim, out_features=embed_dim, bias=False
137+
)
138+
self.k_proj = nn.Linear(
139+
in_features=embed_dim, out_features=embed_dim, bias=False
140+
)
141+
self.v_proj = nn.Linear(
142+
in_features=embed_dim, out_features=embed_dim, bias=False
143+
)
144+
self.out_proj = nn.Linear(
145+
in_features=embed_dim, out_features=embed_dim, bias=False
146+
)
147+
self.dropout = nn.Dropout(p=dropout_p)
148+
149+
if attention_type == "entmax15":
150+
self._attn_fn = entmax15
151+
elif attention_type == "sparsemax":
152+
self._attn_fn = sparsemax
153+
else:
154+
self._attn_fn = partial(F.softmax, dim=-1)
155+
156+
def forward(self, x: Tensor) -> Tensor:
157+
b, s, _ = x.shape
158+
159+
q = self.q_proj(x).view(b, s, self.num_heads, self.head_dim).transpose(1, 2)
160+
k = self.k_proj(x).view(b, s, self.num_heads, self.head_dim).transpose(1, 2)
161+
v = self.v_proj(x).view(b, s, self.num_heads, self.head_dim).transpose(1, 2)
162+
163+
attn = (q @ k.transpose(-2, -1)) * self.scale
164+
attn = self._attn_fn(attn, dim=-1)
165+
attn = self.dropout(attn)
166+
167+
out = (attn @ v).transpose(1, 2).contiguous().view(b, s, self.embed_dim)
168+
return self.out_proj(out)
169+
10170

11171
class LinearAttention(nn.Module):
12172
"""

src/eir/models/output/tabular/shared_mlp_residual.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def __init__(
8181
self.input_identity = nn.Identity()
8282
self.output_identity = nn.Identity()
8383

84+
if model_config.expert_groups is not None:
85+
for group_name in self._batched_group_names:
86+
self.add_module(f"{group_name}_output", nn.Identity())
87+
8488
def _build_shared(self, input_dimension: int) -> None:
8589
task_resblocks_kwargs: dict[str, float | int | bool] = {
8690
"in_features": self.model_config.fc_task_dim,
@@ -276,7 +280,10 @@ def _forward_batched(
276280
)
277281
):
278282
group_out = projected[i, :, : sum(sizes)]
283+
cur_expert_name = self._batched_group_names[i]
284+
group_out = getattr(self, f"{cur_expert_name}_output")(group_out)
279285
split = torch.split(group_out, sizes, dim=1)
286+
280287
for target_name, target_tensor in zip(targets, split, strict=False):
281288
results[target_name] = target_tensor
282289

0 commit comments

Comments
 (0)