File size: 11,498 Bytes
4d74557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
HSE (Hierarchical Sparse Expert) FFN Layer.

Two-tier MoE:
  Tier 1: Select 1 semantic domain from K1=4 domains
  Tier 2: Select top-2 specialists from K2=8 experts within that domain
  Total: 32 experts, 2 active per token

Load balanced at both tiers independently for stable training.
"""

from __future__ import annotations
import math
from typing import Optional, Tuple, Dict

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

from .dgn_norm import build_norm
from .triton_kernels import fused_swiglu


class FeedForwardExpert(nn.Module):
    """Single SwiGLU expert in the HSE layer."""
    
    def __init__(self, d_model: int, d_ff: int):
        super().__init__()
        self.gate = nn.Linear(d_model, d_ff, bias=False)
        self.up   = nn.Linear(d_model, d_ff, bias=False)
        self.down = nn.Linear(d_ff, d_model, bias=False)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.down(fused_swiglu(self.gate(x), self.up(x)))


class DenseSwiGLUFFN(nn.Module):
    """Standard Dense SwiGLU FFN (used in alternating layers)."""
    
    def __init__(
        self,
        d_model: int = 1280,
        d_ff: int = 3200,
        norm_type: str = "dgn",
        dgn_n_groups: int = 16,
        norm_eps: float = 1e-6,
    ):
        super().__init__()
        self.norm = build_norm(norm_type, d_model, dgn_n_groups, norm_eps)
        self.gate = nn.Linear(d_model, d_ff, bias=False)
        self.up   = nn.Linear(d_model, d_ff, bias=False)
        self.down = nn.Linear(d_ff, d_model, bias=False)
        
        nn.init.normal_(self.gate.weight, std=0.02)
        nn.init.normal_(self.up.weight,   std=0.02)
        nn.init.normal_(self.down.weight, std=0.02 / math.sqrt(2))
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        residual = x
        h = self.norm(x)
        return residual + self.down(fused_swiglu(self.gate(h), self.up(h)))


class HSELayer(nn.Module):
    """
    Hierarchical Sparse Expert FFN Layer.
    
    Architecture:
      pre-norm β†’ Tier1-router β†’ select domain d*
             β†’ Tier2-router[d*] β†’ select top-2 experts
             β†’ weighted sum of 2 expert outputs
             β†’ residual add
    """
    
    def __init__(
        self,
        d_model: int = 1280,
        n_domains: int = 4,
        n_experts_per_domain: int = 8,
        top_k: int = 2,
        d_ff_expert: int = 288,
        capacity_factor: float = 1.25,
        expert_dropout: float = 0.1,
        aux_loss_alpha: float = 0.01,
        gumbel_tau_init: float = 1.0,
        norm_type: str = "dgn",
        dgn_n_groups: int = 16,
        norm_eps: float = 1e-6,
        layer_idx: int = 0,
    ):
        super().__init__()
        assert top_k <= n_experts_per_domain
        
        self.d_model = d_model
        self.n_domains = n_domains
        self.n_experts_per_domain = n_experts_per_domain
        self.n_experts_total = n_domains * n_experts_per_domain
        self.top_k = top_k
        self.d_ff_expert = d_ff_expert
        self.capacity_factor = capacity_factor
        self.expert_dropout = expert_dropout
        self.aux_loss_alpha = aux_loss_alpha
        self.layer_idx = layer_idx
        
        # Pre-norm
        self.norm = build_norm(norm_type, d_model, dgn_n_groups, norm_eps)
        
        # ── Tier-1 Router ────────────────────────────────────────────────────
        # Selects semantic domain: d* = argmax(W_r1 Β· x / Ο„)
        self.tier1_router = nn.Linear(d_model, n_domains, bias=False)
        self.gumbel_tau = nn.Parameter(
            torch.tensor(gumbel_tau_init), requires_grad=False  # annealed externally
        )
        
        # ── Tier-2 Routers (one per domain) ──────────────────────────────────
        self.tier2_routers = nn.ModuleList([
            nn.Linear(d_model, n_experts_per_domain, bias=False)
            for _ in range(n_domains)
        ])
        
        # ── Experts ───────────────────────────────────────────────────────────
        self.experts = nn.ModuleList([
            FeedForwardExpert(d_model, d_ff_expert)
            for _ in range(self.n_experts_total)
        ])
        
        # ── Dropout for expert regularization ────────────────────────────────
        self.expert_dropout_layer = nn.Dropout(expert_dropout)
        
        # ── Aux loss accumulator (for logging) ────────────────────────────────
        self._last_aux_loss: Optional[torch.Tensor] = None
        
        self._init_weights()
    
    def _init_weights(self):
        """Initialize router weights to near-uniform distribution."""
        nn.init.normal_(self.tier1_router.weight, std=0.02)
        for router in self.tier2_routers:
            nn.init.normal_(router.weight, std=0.02)
        for expert in self.experts:
            nn.init.normal_(expert.gate.weight, std=0.02)
            nn.init.normal_(expert.up.weight, std=0.02)
            nn.init.normal_(expert.down.weight, std=0.02 / math.sqrt(2))
    
    def update_gumbel_tau(self, progress: float):
        """
        Anneal Gumbel-softmax temperature from init to final.
        progress: float in [0, 1] representing training progress.
        """
        tau_init = 1.0
        tau_final = 0.1
        tau = tau_init * (tau_final / tau_init) ** progress
        self.gumbel_tau.data.fill_(tau)
    
    def _compute_aux_loss(
        self,
        tier1_probs: torch.Tensor,  # (N, n_domains) 
        domain_idx: torch.Tensor,   # (N,)
        x_flat: torch.Tensor,       # (N, d_model)
    ) -> torch.Tensor:
        """
        Hierarchical load-balance auxiliary loss.
        
        Loss = Ξ± * [Tier1_balance + Tier2_balance]
        
        Tier1_balance = n_domains * Ξ£_d [f_d * P_d]
        Tier2_balance = Ξ£_d {n_exp * Ξ£_e [f_{d,e} * P_{d,e}]} (over assigned tokens)
        """
        N = x_flat.shape[0]
        
        # Tier-1 balance
        # f_d: fraction of tokens routed to domain d
        one_hot_d = F.one_hot(domain_idx, self.n_domains).float()  # (N, n_domains)
        f1 = one_hot_d.mean(0)                                      # (n_domains,)
        P1 = tier1_probs.mean(0)                                    # (n_domains,)
        loss_t1 = self.n_domains * (f1 * P1).sum()
        
        # Tier-2 balance (over tokens in each domain)
        loss_t2 = torch.zeros(1, device=x_flat.device)
        
        for d in range(self.n_domains):
            mask = (domain_idx == d)
            n_d = mask.sum()
            if n_d < 2:
                continue
            
            x_d = x_flat[mask]                                      # (n_d, d_model)
            t2_logits = self.tier2_routers[d](x_d)                 # (n_d, n_exp)
            t2_probs = F.softmax(t2_logits, dim=-1)                 # (n_d, n_exp)
            
            # Top-k indices for f computation
            topk_idx = t2_logits.topk(self.top_k, dim=-1).indices  # (n_d, top_k)
            one_hot_e = F.one_hot(topk_idx, self.n_experts_per_domain).float()  # (n_d, k, n_exp)
            f2 = one_hot_e.sum(1).mean(0) / self.top_k             # (n_exp,)
            P2 = t2_probs.mean(0)                                   # (n_exp,)
            
            loss_t2 = loss_t2 + self.n_experts_per_domain * (f2 * P2).sum()
        
        return self.aux_loss_alpha * (loss_t1 + loss_t2)
    
    def forward(
        self,
        x: torch.Tensor,  # (B, T, d_model)
        return_aux_loss: bool = True,
    ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
        """
        Forward pass with hierarchical routing (vectorized for maximum GPU throughput).
        Returns (output, aux_loss) where aux_loss is for load balancing.
        """
        B, T, D = x.shape
        N = B * T
        
        residual = x
        x_norm = self.norm(x)
        x_flat = x_norm.reshape(N, D)  # (N, D)
        
        # ── Tier-1 Routing ────────────────────────────────────────────────────
        tier1_logits = self.tier1_router(x_flat)              # (N, n_domains)
        tier1_probs = F.softmax(tier1_logits / float(self.gumbel_tau), dim=-1)
        
        if self.training:
            domain_idx = F.gumbel_softmax(
                tier1_logits, 
                tau=float(self.gumbel_tau), 
                hard=True
            ).argmax(dim=-1)                                   # (N,)
        else:
            domain_idx = tier1_probs.argmax(dim=-1)            # (N,)
        
        # ── Tier-2 Routing (Zero-sync batched projection + gather) ───────────
        t2_weights = torch.cat([r.weight for r in self.tier2_routers], dim=0)  # (32, D)
        t2_all = F.linear(x_flat, t2_weights).view(N, self.n_domains, self.n_experts_per_domain)
        
        d_expanded = domain_idx.view(N, 1, 1).expand(-1, 1, self.n_experts_per_domain)
        t2_logits = torch.gather(t2_all, 1, d_expanded).squeeze(1)  # (N, 8)
        
        t2_probs = F.softmax(t2_logits, dim=-1)
        w, topk_idx = t2_probs.topk(self.top_k, dim=-1)
        w = (w / (w.sum(-1, keepdim=True) + 1e-9)).to(x.dtype)
        
        global_exp_idx = topk_idx + (domain_idx.unsqueeze(-1) * self.n_experts_per_domain)  # (N, 2)
        
        # ── Vectorized Token-Expert Dispatch ──────────────────────────────────
        flat_exp_ids = global_exp_idx.reshape(-1)
        flat_tokens = torch.arange(N, device=x.device).unsqueeze(1).expand(-1, self.top_k).reshape(-1)
        flat_weights = w.reshape(-1, 1)
        
        sort_perm = torch.argsort(flat_exp_ids)
        sorted_exp = flat_exp_ids[sort_perm]
        sorted_tokens = flat_tokens[sort_perm]
        sorted_weights = flat_weights[sort_perm]
        
        counts = torch.bincount(sorted_exp, minlength=self.n_experts_total).tolist()
        out_flat = torch.zeros(N, D, device=x.device, dtype=x.dtype)
        offset = 0
        
        for e in range(self.n_experts_total):
            cnt = counts[e]
            if cnt > 0:
                t_idx = sorted_tokens[offset : offset + cnt]
                w_e = sorted_weights[offset : offset + cnt]
                x_e = x_flat[t_idx]
                if self.training and self.expert_dropout > 0:
                    x_e = self.expert_dropout_layer(x_e)
                y_e = self.experts[e](x_e)
                out_flat.index_add_(0, t_idx, w_e * y_e)
                offset += cnt
                
        output = out_flat.reshape(B, T, D)
        
        # ── Auxiliary loss ────────────────────────────────────────────────────
        aux_loss = None
        if return_aux_loss and self.training:
            aux_loss = self._compute_aux_loss(tier1_probs, domain_idx, x_flat)
            self._last_aux_loss = aux_loss.detach()
        
        return residual + output, aux_loss