smithblack-0 commited on
Commit
3bfcfea
Β·
verified Β·
1 Parent(s): 102f1bb

Update architecture and tokenizer

Browse files
Files changed (2) hide show
  1. config.json +1 -1
  2. huggingface.py +163 -67
config.json CHANGED
@@ -21,7 +21,7 @@
21
  "rope_mode": "main_sequence",
22
  "tie_word_embeddings": false,
23
  "training_sequence_length": 1024,
24
- "transformers_version": "5.12.1",
25
  "use_cache": true,
26
  "use_residual_gate": true,
27
  "vocab_size": 50277,
 
21
  "rope_mode": "main_sequence",
22
  "tie_word_embeddings": false,
23
  "training_sequence_length": 1024,
24
+ "transformers_version": "5.14.1",
25
  "use_cache": true,
26
  "use_residual_gate": true,
27
  "vocab_size": 50277,
huggingface.py CHANGED
@@ -44,6 +44,7 @@ from torch import nn
44
  from torch.nn.attention.flex_attention import create_block_mask
45
  from torch.nn.attention.flex_attention import flex_attention
46
  import torch.nn.functional as F
 
47
 
48
 
49
 
@@ -671,15 +672,14 @@ class MoSRAHCache(CacheLayerMixin):
671
  "Use get_heads_lengths() for per-head occupancy."
672
  )
673
 
674
- def get_max_cache_shape(self) -> int: # type: ignore[override]
675
- """Return the static per-(batch, head) slot capacity of this cache.
676
-
677
- Equal to mosrah_cache_length as supplied at construction, which is derived
678
- from config.mosrah_cache_length. Required by the HuggingFace static cache
679
- contract; generation machinery uses this to size attention masks.
680
- """
681
  return self.mosrah_cache_length
682
 
 
 
 
 
683
  def get_mask_sizes( # type: ignore[override]
684
  self,
685
  cache_position: torch.Tensor,
@@ -987,9 +987,13 @@ class RouterCache(CacheLayerMixin):
987
  """Not supported β€” RouterCache tracks block position, not sequence length."""
988
  raise NotImplementedError("RouterCache does not track sequence length.")
989
 
 
 
 
 
990
  def get_max_cache_shape(self) -> int:
991
- """Not supported β€” RouterCache does not hold KV pairs."""
992
- raise NotImplementedError("RouterCache does not have a KV cache shape.")
993
 
994
  def get_mask_sizes( # type: ignore[override]
995
  self,
@@ -1251,9 +1255,14 @@ class LocalSlidingWindowLayerCache(CacheLayerMixin):
1251
  """
1252
  return int(self._total_processed)
1253
 
1254
- def get_max_cache_shape(self) -> int:
 
1255
  return self.sliding_window
1256
 
 
 
 
 
1257
  def get_mask_sizes( # type: ignore[override]
1258
  self,
1259
  cache_position: torch.Tensor,
@@ -1515,16 +1524,18 @@ class ShramLayerCache(CacheLayerMixin):
1515
  "Update sliding_window_cache or mosrah_cache directly."
1516
  )
1517
 
1518
- def get_max_cache_shape(self) -> int: # type: ignore[override]
1519
  """Return the maximum sequence length this layer cache can serve.
1520
 
1521
  The authoritative upper bound is ``config.inference_sequence_length``, which
1522
  governs the full accumulated token history the model is configured to handle.
1523
- HuggingFace's static-cache machinery reads this value to determine whether the
1524
- cache is compileable and to size generation loops.
1525
  """
1526
  return self._inference_sequence_length
1527
 
 
 
 
 
1528
  def get_mask_sizes( # type: ignore[override]
1529
  self,
1530
  cache_position: torch.Tensor,
@@ -2378,6 +2389,54 @@ accumulated cached state returned by that cache.
2378
 
2379
 
2380
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2381
 
2382
 
2383
 
@@ -2527,9 +2586,13 @@ class BottleneckedEnsembleAttention(nn.Module):
2527
  return torch.einsum("bltu,lud->bltd", attended_states, self.o_proj)
2528
 
2529
  def _reset_parameters(self) -> None:
2530
- """Initialize per-head projection weights."""
 
 
 
 
2531
  for weight in (self.q_proj, self.k_proj, self.v_proj, self.o_proj):
2532
- nn.init.xavier_uniform_(weight)
2533
 
2534
  def _validate_tensor_shape(self, packed_embeddings: torch.Tensor) -> None:
2535
  """Validate the local packed-embedding shape contract required by BEA."""
@@ -2979,8 +3042,10 @@ input hidden state x, the router produces two outputs used downstream:
2979
  - selected_heads (I): which K of the L available expert heads each token
2980
  routes to, determined by a block-balanced causal solver.
2981
  - routing_probs (P): the weights used for the weighted output reduction,
2982
- gathered from the softmax routing scores at the selected indices and
2983
- renormalized to sum to 1 per token.
 
 
2984
 
2985
  Routing uses a single learnable projection:
2986
 
@@ -3009,6 +3074,11 @@ Paper ref: Appendix A.Routing.
3009
 
3010
 
3011
 
 
 
 
 
 
3012
  class MoSRAHRouter(nn.Module):
3013
  """Token-choice router for MoSRAH sparse attention.
3014
 
@@ -3017,8 +3087,8 @@ class MoSRAHRouter(nn.Module):
3017
  consecutive tokens every expert is used exactly once, giving perfect load
3018
  balance by construction.
3019
 
3020
- routing_weight is nn.Parameter rather than nn.Linear so that HuggingFace
3021
- _init_weights does not override its kaiming initialization at construction.
3022
 
3023
  Attributes:
3024
  routing_weight: Shape (L, embedding_width). Maps input hidden states to
@@ -3034,16 +3104,16 @@ class MoSRAHRouter(nn.Module):
3034
 
3035
  def __init__(self, config: ShramConfig) -> None:
3036
  super().__init__()
3037
- self.num_mosrah_heads = config.num_mosrah_heads
3038
  self.num_selected_heads = config.num_selected_heads
3039
- self.block_length = config.block_length
 
3040
 
3041
  # Routing projection: maps input (B, N, d) to per-head routing scores (B, N, L).
3042
- # nn.Parameter ensures HuggingFace _init_weights does not override kaiming init.
3043
  self.routing_weight = nn.Parameter(
3044
  torch.empty(config.num_mosrah_heads, config.embedding_width)
3045
  )
3046
- nn.init.kaiming_normal_(self.routing_weight)
3047
 
3048
  def forward(
3049
  self,
@@ -3062,16 +3132,17 @@ class MoSRAHRouter(nn.Module):
3062
  Returns:
3063
  selected_heads: Head indices I of shape (batch, seq_len, num_selected_heads).
3064
  Each token's K selected head indices from the block-balanced solver.
3065
- routing_probs: Routing probabilities P of shape (batch, seq_len,
3066
- num_selected_heads). Gathered from the pre-balance softmax at
3067
- selected_heads and renormalized to sum to 1 per token.
 
3068
  router_diagnostics: Dict of routing scalars:
3069
  - ``regret_loss``: gradient-carrying mean regret, mean of
3070
  max(p_max_active βˆ’ p_chosen, 0) over live (B, num_blocks, L)
3071
  entries. In [0, 1]. Zero when every expert is assigned at its
3072
  peak-preference token within the block.
3073
  - ``logit_regret``: detached logit-space regret; same formula
3074
- applied to routing logits rather than softmax probabilities.
3075
  In [0, ∞). Monitoring only.
3076
  - ``logit_std``: detached mean per-token std of routing logits.
3077
  """
@@ -3101,13 +3172,18 @@ class MoSRAHRouter(nn.Module):
3101
 
3102
  # ── Phase: pre-balance scoring ─────────────────────────────────────────
3103
  #
3104
- # Establishes the clean routing distribution before any -inf masking.
3105
- # logit_std is captured here because the block solver's masking would
3106
- # corrupt the standard deviation. routing_scores is used both for
3107
- # regret_loss and for the final routing_probs.
3108
  routing_logits = self._compute_routing_logits(x) # (B, N, L)
3109
- logit_std = routing_logits.std(dim=-1).mean().detach()
3110
- routing_scores = F.softmax(routing_logits, dim=-1) # (B, N, L)
 
 
 
 
 
3111
 
3112
  # ── Phase: block-balanced causal selection ─────────────────────────────
3113
  #
@@ -3136,9 +3212,9 @@ class MoSRAHRouter(nn.Module):
3136
  # token per "block" there is no alternative assignment with higher
3137
  # preference.
3138
  used_in_block = router_cache.get_used_in_block() # (B, L)
3139
- step_logits = routing_logits[:, 0, :] # (B, L)
3140
- available = step_logits.masked_fill(used_in_block, float('-inf'))
3141
- step_heads = available.topk(K, dim=-1).indices # (B, K)
3142
 
3143
  router_cache.update_decode(step_heads)
3144
 
@@ -3152,8 +3228,8 @@ class MoSRAHRouter(nn.Module):
3152
  # preceding them have already had their pick each step. The pad is
3153
  # discarded after the solver.
3154
  num_blocks = (N + W - 1) // W
3155
- N_pad = num_blocks * W
3156
- pad_len = N_pad - N
3157
 
3158
  if pad_len > 0:
3159
  padded_logits = torch.cat(
@@ -3167,14 +3243,20 @@ class MoSRAHRouter(nn.Module):
3167
  # used_in_block tracks which experts have been claimed within each block.
3168
  # No gradient here β€” expert availability is a hard structural constraint,
3169
  # not a differentiable quantity. Gradient flows through routing_probs.
3170
- used_in_block = torch.zeros(B, num_blocks, L, dtype=torch.bool, device=x.device)
 
 
 
 
 
 
3171
  step_heads_list = []
3172
 
3173
  for step in range(W):
3174
  step_logits = blocked_logits[:, :, step, :] # (B, blk, L)
3175
 
3176
  # Claimed experts receive -inf so top-K never selects them.
3177
- available = step_logits.masked_fill(used_in_block, float('-inf'))
3178
  step_heads = available.topk(K, dim=-1).indices # (B, blk, K)
3179
  step_heads_list.append(step_heads)
3180
 
@@ -3183,7 +3265,7 @@ class MoSRAHRouter(nn.Module):
3183
 
3184
  # Stack W steps and reshape to (B, N_pad, K), then unpad.
3185
  selected_heads_blocked = torch.stack(step_heads_list, dim=2) # (B, blk, W, K)
3186
- selected_heads = selected_heads_blocked.view(B, N_pad, K)[:, :N, :] # (B, N, K)
3187
 
3188
  if router_cache is not None:
3189
  # Prefill: persist the partial last-block state so decode steps
@@ -3203,27 +3285,38 @@ class MoSRAHRouter(nn.Module):
3203
  # blocks, and a single decode step is not a complete block. Backward is
3204
  # never called during inference so the zero is a correct no-op.
3205
  if router_cache is not None and N == 1:
3206
- regret_loss = routing_logits.new_zeros(())
3207
- logit_regret = routing_logits.new_zeros(()).detach()
3208
  else:
3209
  regret_loss, logit_regret = self._compute_regret(
3210
- routing_scores,
3211
- routing_logits,
3212
  selected_heads_blocked,
3213
  active_mask,
3214
  )
3215
 
3216
- # ── Phase: routing probabilities ────────────────────────────────────────
3217
  #
3218
- # Gathered from the pre-balance routing_scores to reflect genuine routing
3219
- # preference; renormalized so they sum to 1 per token.
3220
- gathered = routing_scores.gather(dim=-1, index=selected_heads) # (B, N, K)
3221
- routing_probs = gathered / gathered.sum(dim=-1, keepdim=True) # (B, N, K)
 
 
 
 
 
 
 
 
 
 
 
3222
 
3223
  router_diagnostics = {
3224
- "regret_loss": regret_loss,
3225
  "logit_regret": logit_regret,
3226
- "logit_std": logit_std,
3227
  }
3228
  return selected_heads, routing_probs, router_diagnostics
3229
 
@@ -3260,9 +3353,9 @@ class MoSRAHRouter(nn.Module):
3260
  detached β€” it is a monitoring scalar only, in [0, ∞).
3261
 
3262
  Args:
3263
- routing_scores: Softmax routing probabilities, shape (B, N, L).
3264
  Gradient flows through this tensor into regret_loss.
3265
- routing_logits: Pre-softmax routing logits, shape (B, N, L).
3266
  Used only for the detached logit_regret.
3267
  selected_heads_blocked: Expert assignments from the block solver,
3268
  shape (B, num_blocks, W, K). Block geometry
@@ -3274,8 +3367,8 @@ class MoSRAHRouter(nn.Module):
3274
  logit_regret: Detached scalar in [0, ∞).
3275
  """
3276
  B, num_blocks, W, _K = selected_heads_blocked.shape
3277
- L = routing_scores.shape[-1]
3278
- N = routing_scores.shape[1]
3279
  N_pad = num_blocks * W
3280
 
3281
  # ── Reshape into block form ─────────────────────────────────────────
@@ -3311,23 +3404,25 @@ class MoSRAHRouter(nn.Module):
3311
  assignment_mask.scatter_(dim=-1, index=selected_heads_blocked, value=1.0)
3312
  # (B, nb, W, L)
3313
 
3314
- # ── Prob regret (gradient flows through routing_scores) ─────────────
3315
  #
3316
  # p_chosen: routing score at the assigned token, gated by active_float
3317
  # so dead assignments contribute 0 β€” the expert accrues full regret
3318
  # against the active maximum rather than no penalty.
3319
  # p_max: peak routing score over active tokens; dead tokens zeroed before
3320
- # max (safe because softmax outputs are non-negative).
3321
- p_chosen = (assignment_mask * active_float.unsqueeze(-1) * scores_blocked).sum(dim=2)
3322
- # (B, nb, L)
3323
- p_max = (active_float.unsqueeze(-1) * scores_blocked).max(dim=2).values
3324
- # (B, nb, L)
 
 
3325
 
3326
- regret = (p_max - p_chosen).clamp(min=0.0) # (B, nb, L)
3327
 
3328
  # Mean over live (B, num_blocks, L) entries. Clamped to 1 for the
3329
  # all-dead edge case where the numerator is already 0.
3330
- num_live = block_active.float().sum() # scalar
3331
  regret_loss = (
3332
  block_active.float().unsqueeze(-1) * regret
3333
  ).sum() / num_live.mul(L).clamp(min=1.0)
@@ -3343,12 +3438,13 @@ class MoSRAHRouter(nn.Module):
3343
  ).sum(dim=2) # (B, nb, L)
3344
 
3345
  logit_max = logits_blocked.masked_fill(
3346
- ~active_blocked.unsqueeze(-1), float('-inf')
3347
- ).max(dim=2).values # (B, nb, L)
3348
  logit_max = logit_max.masked_fill(~block_active.unsqueeze(-1), 0.0)
3349
 
3350
  logit_regret = (
3351
- block_active.float().unsqueeze(-1) * (logit_max - logit_chosen).clamp(min=0.0)
 
3352
  ).sum() / num_live.mul(L).clamp(min=1.0)
3353
  logit_regret = logit_regret.detach()
3354
 
 
44
  from torch.nn.attention.flex_attention import create_block_mask
45
  from torch.nn.attention.flex_attention import flex_attention
46
  import torch.nn.functional as F
47
+ from entmax import entmax_bisect
48
 
49
 
50
 
 
672
  "Use get_heads_lengths() for per-head occupancy."
673
  )
674
 
675
+ def get_max_length(self) -> int:
676
+ """Return the static per-(batch, head) slot capacity of this cache."""
 
 
 
 
 
677
  return self.mosrah_cache_length
678
 
679
+ def get_max_cache_shape(self) -> int: # type: ignore[override]
680
+ """Compatibility alias for the deprecated cache-shape interface."""
681
+ return self.get_max_length()
682
+
683
  def get_mask_sizes( # type: ignore[override]
684
  self,
685
  cache_position: torch.Tensor,
 
987
  """Not supported β€” RouterCache tracks block position, not sequence length."""
988
  raise NotImplementedError("RouterCache does not track sequence length.")
989
 
990
+ def get_max_length(self) -> int:
991
+ """Return an undefined maximum because no token sequence is stored."""
992
+ return -1
993
+
994
  def get_max_cache_shape(self) -> int:
995
+ """Compatibility alias for the deprecated cache-shape interface."""
996
+ return self.get_max_length()
997
 
998
  def get_mask_sizes( # type: ignore[override]
999
  self,
 
1255
  """
1256
  return int(self._total_processed)
1257
 
1258
+ def get_max_length(self) -> int:
1259
+ """Return the retained local sliding-window capacity."""
1260
  return self.sliding_window
1261
 
1262
+ def get_max_cache_shape(self) -> int:
1263
+ """Compatibility alias for the deprecated cache-shape interface."""
1264
+ return self.get_max_length()
1265
+
1266
  def get_mask_sizes( # type: ignore[override]
1267
  self,
1268
  cache_position: torch.Tensor,
 
1524
  "Update sliding_window_cache or mosrah_cache directly."
1525
  )
1526
 
1527
+ def get_max_length(self) -> int:
1528
  """Return the maximum sequence length this layer cache can serve.
1529
 
1530
  The authoritative upper bound is ``config.inference_sequence_length``, which
1531
  governs the full accumulated token history the model is configured to handle.
 
 
1532
  """
1533
  return self._inference_sequence_length
1534
 
1535
+ def get_max_cache_shape(self) -> int: # type: ignore[override]
1536
+ """Compatibility alias for the deprecated cache-shape interface."""
1537
+ return self.get_max_length()
1538
+
1539
  def get_mask_sizes( # type: ignore[override]
1540
  self,
1541
  cache_position: torch.Tensor,
 
2389
 
2390
 
2391
 
2392
+ # -----------
2393
+ # Inlined from: initialization.py
2394
+ # -----------
2395
+ """Initialization policy for SHRAM raw projection parameters.
2396
+
2397
+ Hugging Face initializes ordinary ``nn.Linear`` and ``nn.Embedding`` modules.
2398
+ SHRAM also stores two custom raw-parameter forms that Hugging Face cannot infer:
2399
+
2400
+ - a three-dimensional bank of independent expert linear maps
2401
+ - a two-dimensional router projection
2402
+
2403
+ The expert-bank leading dimension is ownership/storage, not fan geometry. Each
2404
+ expert matrix must therefore receive Xavier initialization independently. The
2405
+ router follows the proven BALANCE initialization scale instead. Tensor rank is
2406
+ used here as the explicit representation contract for these two owners, not as
2407
+ a general initialization heuristic. Adding another raw projection shape must
2408
+ therefore extend this contract deliberately rather than inheriting a fallback.
2409
+ """
2410
+
2411
+
2412
+
2413
+
2414
+
2415
+ ROUTER_INIT_STD = 0.02
2416
+
2417
+
2418
+ @torch.no_grad()
2419
+ def initialize_projection_parameter(parameter: nn.Parameter) -> None:
2420
+ """Initialize one of SHRAM's supported raw projection representations.
2421
+
2422
+ A rank-three tensor is an independent bank of linear matrices and is
2423
+ initialized matrix-by-matrix with Xavier uniform. A rank-two tensor is the
2424
+ router projection and receives the BALANCE-normal initialization.
2425
+ """
2426
+ if parameter.ndim == 3:
2427
+ for matrix in parameter.unbind(dim=0):
2428
+ nn.init.xavier_uniform_(matrix)
2429
+ return
2430
+
2431
+ if parameter.ndim == 2:
2432
+ nn.init.normal_(parameter, mean=0.0, std=ROUTER_INIT_STD)
2433
+ return
2434
+
2435
+ raise ValueError(
2436
+ "SHRAM raw projection parameters must be rank two (router) or rank "
2437
+ f"three (independent expert bank), got shape {tuple(parameter.shape)}."
2438
+ )
2439
+
2440
 
2441
 
2442
 
 
2586
  return torch.einsum("bltu,lud->bltd", attended_states, self.o_proj)
2587
 
2588
  def _reset_parameters(self) -> None:
2589
+ """Initialize projection banks independently of storage rank.
2590
+
2591
+ These tensors store independent per-head linear maps. Their leading
2592
+ head dimension is storage organization rather than fan geometry.
2593
+ """
2594
  for weight in (self.q_proj, self.k_proj, self.v_proj, self.o_proj):
2595
+ initialize_projection_parameter(weight)
2596
 
2597
  def _validate_tensor_shape(self, packed_embeddings: torch.Tensor) -> None:
2598
  """Validate the local packed-embedding shape contract required by BEA."""
 
3042
  - selected_heads (I): which K of the L available expert heads each token
3043
  routes to, determined by a block-balanced causal solver.
3044
  - routing_probs (P): the weights used for the weighted output reduction,
3045
+ gathered from the Entmax routing distribution at the selected indices.
3046
+ Tokens with positive selected mass are renormalized to sum to one; tokens
3047
+ whose mechanically assigned experts all have zero Entmax mass receive an
3048
+ all-zero mixture and therefore no sparse-path contribution.
3049
 
3050
  Routing uses a single learnable projection:
3051
 
 
3074
 
3075
 
3076
 
3077
+
3078
+
3079
+ ENTMAX_ALPHA = 1.2
3080
+
3081
+
3082
  class MoSRAHRouter(nn.Module):
3083
  """Token-choice router for MoSRAH sparse attention.
3084
 
 
3087
  consecutive tokens every expert is used exactly once, giving perfect load
3088
  balance by construction.
3089
 
3090
+ routing_weight is an ``nn.Parameter`` rather than ``nn.Linear`` because the
3091
+ router owns a custom raw projection boundary and its initialization.
3092
 
3093
  Attributes:
3094
  routing_weight: Shape (L, embedding_width). Maps input hidden states to
 
3104
 
3105
  def __init__(self, config: ShramConfig) -> None:
3106
  super().__init__()
3107
+ self.num_mosrah_heads = config.num_mosrah_heads
3108
  self.num_selected_heads = config.num_selected_heads
3109
+ self.block_length = config.block_length
3110
+ self.entmax_alpha = ENTMAX_ALPHA
3111
 
3112
  # Routing projection: maps input (B, N, d) to per-head routing scores (B, N, L).
 
3113
  self.routing_weight = nn.Parameter(
3114
  torch.empty(config.num_mosrah_heads, config.embedding_width)
3115
  )
3116
+ initialize_projection_parameter(self.routing_weight)
3117
 
3118
  def forward(
3119
  self,
 
3132
  Returns:
3133
  selected_heads: Head indices I of shape (batch, seq_len, num_selected_heads).
3134
  Each token's K selected head indices from the block-balanced solver.
3135
+ routing_probs: Routing weights P of shape (batch, seq_len,
3136
+ num_selected_heads). Selected positive Entmax mass is normalized
3137
+ to one. An all-zero selected support remains all-zero, allowing
3138
+ the token to make no sparse-path contribution.
3139
  router_diagnostics: Dict of routing scalars:
3140
  - ``regret_loss``: gradient-carrying mean regret, mean of
3141
  max(p_max_active βˆ’ p_chosen, 0) over live (B, num_blocks, L)
3142
  entries. In [0, 1]. Zero when every expert is assigned at its
3143
  peak-preference token within the block.
3144
  - ``logit_regret``: detached logit-space regret; same formula
3145
+ applied to routing logits rather than Entmax probabilities.
3146
  In [0, ∞). Monitoring only.
3147
  - ``logit_std``: detached mean per-token std of routing logits.
3148
  """
 
3172
 
3173
  # ── Phase: pre-balance scoring ─────────────────────────────────────────
3174
  #
3175
+ # Establish the clean routing distribution before any -inf masking.
3176
+ # Entmax and selected-weight normalization are computed in fp32 by
3177
+ # numeric policy. Only the final route weights return to model dtype.
3178
+ # The regret objective also retains the fp32 distribution.
3179
  routing_logits = self._compute_routing_logits(x) # (B, N, L)
3180
+ routing_logits_fp32 = routing_logits.float()
3181
+ logit_std = routing_logits_fp32.std(dim=-1).mean().detach()
3182
+ routing_scores_fp32 = entmax_bisect(
3183
+ routing_logits_fp32,
3184
+ alpha=self.entmax_alpha,
3185
+ dim=-1,
3186
+ ) # (B, N, L), fp32
3187
 
3188
  # ── Phase: block-balanced causal selection ─────────────────────────────
3189
  #
 
3212
  # token per "block" there is no alternative assignment with higher
3213
  # preference.
3214
  used_in_block = router_cache.get_used_in_block() # (B, L)
3215
+ step_logits = routing_logits[:, 0, :] # (B, L)
3216
+ available = step_logits.masked_fill(used_in_block, float("-inf"))
3217
+ step_heads = available.topk(K, dim=-1).indices # (B, K)
3218
 
3219
  router_cache.update_decode(step_heads)
3220
 
 
3228
  # preceding them have already had their pick each step. The pad is
3229
  # discarded after the solver.
3230
  num_blocks = (N + W - 1) // W
3231
+ N_pad = num_blocks * W
3232
+ pad_len = N_pad - N
3233
 
3234
  if pad_len > 0:
3235
  padded_logits = torch.cat(
 
3243
  # used_in_block tracks which experts have been claimed within each block.
3244
  # No gradient here β€” expert availability is a hard structural constraint,
3245
  # not a differentiable quantity. Gradient flows through routing_probs.
3246
+ used_in_block = torch.zeros(
3247
+ B,
3248
+ num_blocks,
3249
+ L,
3250
+ dtype=torch.bool,
3251
+ device=x.device,
3252
+ )
3253
  step_heads_list = []
3254
 
3255
  for step in range(W):
3256
  step_logits = blocked_logits[:, :, step, :] # (B, blk, L)
3257
 
3258
  # Claimed experts receive -inf so top-K never selects them.
3259
+ available = step_logits.masked_fill(used_in_block, float("-inf"))
3260
  step_heads = available.topk(K, dim=-1).indices # (B, blk, K)
3261
  step_heads_list.append(step_heads)
3262
 
 
3265
 
3266
  # Stack W steps and reshape to (B, N_pad, K), then unpad.
3267
  selected_heads_blocked = torch.stack(step_heads_list, dim=2) # (B, blk, W, K)
3268
+ selected_heads = selected_heads_blocked.view(B, N_pad, K)[:, :N, :] # (B, N, K)
3269
 
3270
  if router_cache is not None:
3271
  # Prefill: persist the partial last-block state so decode steps
 
3285
  # blocks, and a single decode step is not a complete block. Backward is
3286
  # never called during inference so the zero is a correct no-op.
3287
  if router_cache is not None and N == 1:
3288
+ regret_loss = routing_logits_fp32.new_zeros(())
3289
+ logit_regret = routing_logits_fp32.new_zeros(()).detach()
3290
  else:
3291
  regret_loss, logit_regret = self._compute_regret(
3292
+ routing_scores_fp32,
3293
+ routing_logits_fp32,
3294
  selected_heads_blocked,
3295
  active_mask,
3296
  )
3297
 
3298
+ # ── Phase: routing weights ──────────────────────────────────────────────
3299
  #
3300
+ # Gather from the clean fp32 Entmax distribution. If mechanical balance
3301
+ # assigns only experts outside a token's sparse Entmax support, the token
3302
+ # intentionally receives an all-zero mixture and contributes nothing on
3303
+ # the sparse path. Positive selected mass is normalized in fp32.
3304
+ gathered_fp32 = routing_scores_fp32.gather( # (B, N, K)
3305
+ dim=-1,
3306
+ index=selected_heads,
3307
+ )
3308
+ gathered_sum = gathered_fp32.sum(dim=-1, keepdim=True)
3309
+ normalization_denominator = gathered_sum.clamp_min(
3310
+ torch.finfo(gathered_fp32.dtype).tiny
3311
+ )
3312
+ routing_probs = (
3313
+ gathered_fp32 / normalization_denominator
3314
+ ).to(dtype=routing_logits.dtype) # (B, N, K)
3315
 
3316
  router_diagnostics = {
3317
+ "regret_loss": regret_loss,
3318
  "logit_regret": logit_regret,
3319
+ "logit_std": logit_std,
3320
  }
3321
  return selected_heads, routing_probs, router_diagnostics
3322
 
 
3353
  detached β€” it is a monitoring scalar only, in [0, ∞).
3354
 
3355
  Args:
3356
+ routing_scores: Entmax routing probabilities, shape (B, N, L).
3357
  Gradient flows through this tensor into regret_loss.
3358
+ routing_logits: Router logits in fp32, shape (B, N, L).
3359
  Used only for the detached logit_regret.
3360
  selected_heads_blocked: Expert assignments from the block solver,
3361
  shape (B, num_blocks, W, K). Block geometry
 
3367
  logit_regret: Detached scalar in [0, ∞).
3368
  """
3369
  B, num_blocks, W, _K = selected_heads_blocked.shape
3370
+ L = routing_scores.shape[-1]
3371
+ N = routing_scores.shape[1]
3372
  N_pad = num_blocks * W
3373
 
3374
  # ── Reshape into block form ─────────────────────────────────────────
 
3404
  assignment_mask.scatter_(dim=-1, index=selected_heads_blocked, value=1.0)
3405
  # (B, nb, W, L)
3406
 
3407
+ # ── Probability regret (gradient flows through routing_scores) ───────
3408
  #
3409
  # p_chosen: routing score at the assigned token, gated by active_float
3410
  # so dead assignments contribute 0 β€” the expert accrues full regret
3411
  # against the active maximum rather than no penalty.
3412
  # p_max: peak routing score over active tokens; dead tokens zeroed before
3413
+ # max (safe because Entmax outputs are non-negative).
3414
+ p_chosen = (
3415
+ assignment_mask * active_float.unsqueeze(-1) * scores_blocked
3416
+ ).sum(dim=2) # (B, nb, L)
3417
+ p_max = (
3418
+ active_float.unsqueeze(-1) * scores_blocked
3419
+ ).max(dim=2).values # (B, nb, L)
3420
 
3421
+ regret = (p_max - p_chosen).clamp(min=0.0) # (B, nb, L)
3422
 
3423
  # Mean over live (B, num_blocks, L) entries. Clamped to 1 for the
3424
  # all-dead edge case where the numerator is already 0.
3425
+ num_live = block_active.float().sum() # scalar
3426
  regret_loss = (
3427
  block_active.float().unsqueeze(-1) * regret
3428
  ).sum() / num_live.mul(L).clamp(min=1.0)
 
3438
  ).sum(dim=2) # (B, nb, L)
3439
 
3440
  logit_max = logits_blocked.masked_fill(
3441
+ ~active_blocked.unsqueeze(-1), float("-inf")
3442
+ ).max(dim=2).values # (B, nb, L)
3443
  logit_max = logit_max.masked_fill(~block_active.unsqueeze(-1), 0.0)
3444
 
3445
  logit_regret = (
3446
+ block_active.float().unsqueeze(-1)
3447
+ * (logit_max - logit_chosen).clamp(min=0.0)
3448
  ).sum() / num_live.mul(L).clamp(min=1.0)
3449
  logit_regret = logit_regret.detach()
3450