smithblack-0 commited on
Commit
4108c4d
Β·
verified Β·
1 Parent(s): 0bc8a52

Update architecture and tokenizer

Browse files
Files changed (4) hide show
  1. README.md +1 -0
  2. config.json +1 -0
  3. configuration.py +13 -0
  4. huggingface.py +308 -55
README.md CHANGED
@@ -84,6 +84,7 @@ contains no weights. All values are overridable via kwargs.
84
  | `inference_sequence_length` | 1024 |
85
  | `load_balance_p` | 2.0 |
86
  | `local_rope_theta` | 10000.0 |
 
87
  | `mlp_width` | 1366 |
88
  | `mosrah_overallocation_factor` | 2.0 |
89
  | `mosrah_rope_theta` | 10000.0 |
 
84
  | `inference_sequence_length` | 1024 |
85
  | `load_balance_p` | 2.0 |
86
  | `local_rope_theta` | 10000.0 |
87
+ | `max_bid_rounds` | 10 |
88
  | `mlp_width` | 1366 |
89
  | `mosrah_overallocation_factor` | 2.0 |
90
  | `mosrah_rope_theta` | 10000.0 |
config.json CHANGED
@@ -11,6 +11,7 @@
11
  "inference_sequence_length": 1024,
12
  "load_balance_p": 2.0,
13
  "local_rope_theta": 10000.0,
 
14
  "mlp_width": 1366,
15
  "model_type": "shram",
16
  "mosrah_overallocation_factor": 2.0,
 
11
  "inference_sequence_length": 1024,
12
  "load_balance_p": 2.0,
13
  "local_rope_theta": 10000.0,
14
+ "max_bid_rounds": 10,
15
  "mlp_width": 1366,
16
  "model_type": "shram",
17
  "mosrah_overallocation_factor": 2.0,
configuration.py CHANGED
@@ -88,6 +88,12 @@ class ShramConfig(PretrainedConfig):
88
  frequencies into the load balance signal. Higher p weights aggregation
89
  toward the worst-case batch item, making the correction signal more
90
  sensitive to per-item allocation spikes. Must be positive. Default 2.0.
 
 
 
 
 
 
91
  """
92
 
93
  model_type = "shram"
@@ -122,6 +128,7 @@ class ShramConfig(PretrainedConfig):
122
  tie_word_embeddings: bool = False,
123
  mosrah_overallocation_factor: float = 2.0,
124
  load_balance_p: float = 2.0,
 
125
  **kwargs
126
  ):
127
  if head_dim % 2 != 0:
@@ -162,6 +169,11 @@ class ShramConfig(PretrainedConfig):
162
  f"load_balance_p must be positive, got {load_balance_p}."
163
  )
164
 
 
 
 
 
 
165
  self.vocab_size = vocab_size
166
  self.embedding_width = embedding_width
167
  self.mlp_width = mlp_width
@@ -181,6 +193,7 @@ class ShramConfig(PretrainedConfig):
181
  self.beta = beta
182
  self.mosrah_overallocation_factor = mosrah_overallocation_factor
183
  self.load_balance_p = load_balance_p
 
184
  self.attention_dropout = attention_dropout
185
  self.use_cache = use_cache
186
 
 
88
  frequencies into the load balance signal. Higher p weights aggregation
89
  toward the worst-case batch item, making the correction signal more
90
  sensitive to per-item allocation spikes. Must be positive. Default 2.0.
91
+ max_bid_rounds: Maximum bidding rounds for the deferred-acceptance capacity
92
+ solver in ``balance_capacity``. 10 covers convergence at approximately
93
+ the 98th percentile of routing densities; the top 2% of extreme-density
94
+ cases are not expected under normal training. The bound exists as a
95
+ correctness guard β€” exhausting it raises ``RuntimeError``. Must be >= 1.
96
+ Default 10.
97
  """
98
 
99
  model_type = "shram"
 
128
  tie_word_embeddings: bool = False,
129
  mosrah_overallocation_factor: float = 2.0,
130
  load_balance_p: float = 2.0,
131
+ max_bid_rounds: int = 10,
132
  **kwargs
133
  ):
134
  if head_dim % 2 != 0:
 
169
  f"load_balance_p must be positive, got {load_balance_p}."
170
  )
171
 
172
+ if max_bid_rounds < 1:
173
+ raise ValueError(
174
+ f"max_bid_rounds must be at least 1, got {max_bid_rounds}."
175
+ )
176
+
177
  self.vocab_size = vocab_size
178
  self.embedding_width = embedding_width
179
  self.mlp_width = mlp_width
 
193
  self.beta = beta
194
  self.mosrah_overallocation_factor = mosrah_overallocation_factor
195
  self.load_balance_p = load_balance_p
196
+ self.max_bid_rounds = max_bid_rounds
197
  self.attention_dropout = attention_dropout
198
  self.use_cache = use_cache
199
 
huggingface.py CHANGED
@@ -175,6 +175,12 @@ class ShramConfig(PretrainedConfig):
175
  frequencies into the load balance signal. Higher p weights aggregation
176
  toward the worst-case batch item, making the correction signal more
177
  sensitive to per-item allocation spikes. Must be positive. Default 2.0.
 
 
 
 
 
 
178
  """
179
 
180
  model_type = "shram"
@@ -209,6 +215,7 @@ class ShramConfig(PretrainedConfig):
209
  tie_word_embeddings: bool = False,
210
  mosrah_overallocation_factor: float = 2.0,
211
  load_balance_p: float = 2.0,
 
212
  **kwargs
213
  ):
214
  if head_dim % 2 != 0:
@@ -249,6 +256,11 @@ class ShramConfig(PretrainedConfig):
249
  f"load_balance_p must be positive, got {load_balance_p}."
250
  )
251
 
 
 
 
 
 
252
  self.vocab_size = vocab_size
253
  self.embedding_width = embedding_width
254
  self.mlp_width = mlp_width
@@ -268,6 +280,7 @@ class ShramConfig(PretrainedConfig):
268
  self.beta = beta
269
  self.mosrah_overallocation_factor = mosrah_overallocation_factor
270
  self.load_balance_p = load_balance_p
 
271
  self.attention_dropout = attention_dropout
272
  self.use_cache = use_cache
273
 
@@ -2458,7 +2471,7 @@ def pack_experts(
2458
  tokens_per_expert = _count_tokens_per_expert(flattened_selected_heads, num_experts)
2459
  max_count = tokens_per_expert.max().item()
2460
  no_overflow = max_count <= packed_length
2461
- _enforce_no_overflow(no_overflow)
2462
 
2463
  # -----------------------------------------------------------------------
2464
  # Construct the unpacking mask.
@@ -2576,7 +2589,7 @@ def unpack_experts(
2576
  # Helpers
2577
  # ---------------------------------------------------------------------------
2578
 
2579
- def _enforce_no_overflow(condition: bool) -> None:
2580
  """Enforce that no expert bucket exceeds the preallocated packed length.
2581
 
2582
  This check fires when the number of tokens assigned to any expert in any
@@ -2602,6 +2615,8 @@ def _enforce_no_overflow(condition: bool) -> None:
2602
  "Expert packing overflow: at least one expert bucket contains more "
2603
  "tokens than mosrah_packed_length allows. Increase "
2604
  "mosrah_overallocation_factor in ShramConfig to resolve."
 
 
2605
  )
2606
 
2607
 
@@ -2797,6 +2812,8 @@ class MoSRAHRouter(nn.Module):
2797
  else:
2798
  self.capacity = config.mosrah_packed_length
2799
 
 
 
2800
  # W_r: routing projection, no bias (paper specifies xW_r, no additional term).
2801
  self.routing_projection = nn.Linear(
2802
  config.embedding_width, config.num_mosrah_heads, bias=False
@@ -2808,63 +2825,293 @@ class MoSRAHRouter(nn.Module):
2808
  self.expert_bias = nn.Parameter(torch.zeros(config.num_mosrah_heads))
2809
 
2810
  @staticmethod
2811
- def balance_capacity(logits: torch.Tensor,
2812
- used_capacity: torch.Tensor | None,
2813
- capacity: int,
2814
- )->torch.Tensor:
 
2815
  """
2816
- Balances capacity limits so that if choosing an
2817
- expert would go over capacity, the expert is simply
2818
- not chosen instead
2819
- :param logits: The logits to balance. (B, N, L)
2820
- :param used_capacity: The used capacity, if it exists. (B, L)
2821
- :param capacity: The maximum available capacity. Int.
2822
- :return: Modified logits.
 
 
 
 
 
 
2823
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2824
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2825
  if used_capacity is None:
2826
- # Presume we are in training mode.
2827
-
2828
- # Looking up capacity limits only
2829
- # matters if it is, in fact, possible
2830
- # to exceed capacity limits.
2831
- if logits.shape[-2] < capacity:
2832
- return logits
2833
-
2834
- # Look up the kthvalue and use that as
2835
- # the threshold to mask when below.
2836
- # Note we negate then negate again to sort
2837
- # in ascending order.
2838
- response = torch.kthvalue(-logits, capacity, dim=-2)
2839
- threshold = -response.values
2840
- threshold = threshold.unsqueeze(-2) #(B, 1, L)
2841
  else:
2842
- # We are operating in inference mode.
2843
- # We have to use padding to accomodate the
2844
- # response physically not being long enough
2845
- # to reach capacity
2846
-
2847
- # Note that padding at zero and shifting
2848
- # the indexes prevents dereferencing a symint,
2849
- # as a version that just patted at 0, 1 and set to
2850
- # length + 1 would do. This prevents a graph break.
2851
- remaining_capacity = capacity - used_capacity # 0 means all used, can be at most capacity
2852
- response_length = logits.shape[-2]
2853
- index = torch.clamp(remaining_capacity, 0, response_length+1)
2854
-
2855
- # Sort, and add padding. Anything asking for a sequence position
2856
- # outside the current sequence will get a threshold of -1e8; always include
2857
- # If we are asking for a value at zero, get 1e8, or full and we include
2858
- # nothing.
2859
- ordered_logits = torch.sort(logits, dim=-2, descending=True).values
2860
- ordered_logits = F.pad(ordered_logits, (0,0, 1, 0), value=1e8)
2861
- ordered_logits = F.pad(ordered_logits, (0, 0, 0, 1), value=-1e8)
2862
-
2863
- threshold = ordered_logits.gather(-2, index.unsqueeze(-2)) #(B, 1, L)
2864
-
2865
- mask = threshold > logits
2866
- logits = logits.masked_fill(mask, -1e8)
2867
- return logits
2868
  def forward(
2869
  self,
2870
  x: torch.Tensor,
@@ -2904,7 +3151,13 @@ class MoSRAHRouter(nn.Module):
2904
  # selection. expert_bias is added to logits before softmax so that the bias
2905
  # shifts selection probability without rescaling the unbiased distribution.
2906
  biased_logits = logits + self.expert_bias
2907
- biased_logits = self.balance_capacity(biased_logits, used_capacity, self.capacity)
 
 
 
 
 
 
2908
  biased_routing_scores = F.softmax( # RΜ‚, (B, N, L)
2909
  biased_logits, dim=-1
2910
  )
 
175
  frequencies into the load balance signal. Higher p weights aggregation
176
  toward the worst-case batch item, making the correction signal more
177
  sensitive to per-item allocation spikes. Must be positive. Default 2.0.
178
+ max_bid_rounds: Maximum bidding rounds for the deferred-acceptance capacity
179
+ solver in ``balance_capacity``. 10 covers convergence at approximately
180
+ the 98th percentile of routing densities; the top 2% of extreme-density
181
+ cases are not expected under normal training. The bound exists as a
182
+ correctness guard β€” exhausting it raises ``RuntimeError``. Must be >= 1.
183
+ Default 10.
184
  """
185
 
186
  model_type = "shram"
 
215
  tie_word_embeddings: bool = False,
216
  mosrah_overallocation_factor: float = 2.0,
217
  load_balance_p: float = 2.0,
218
+ max_bid_rounds: int = 10,
219
  **kwargs
220
  ):
221
  if head_dim % 2 != 0:
 
256
  f"load_balance_p must be positive, got {load_balance_p}."
257
  )
258
 
259
+ if max_bid_rounds < 1:
260
+ raise ValueError(
261
+ f"max_bid_rounds must be at least 1, got {max_bid_rounds}."
262
+ )
263
+
264
  self.vocab_size = vocab_size
265
  self.embedding_width = embedding_width
266
  self.mlp_width = mlp_width
 
280
  self.beta = beta
281
  self.mosrah_overallocation_factor = mosrah_overallocation_factor
282
  self.load_balance_p = load_balance_p
283
+ self.max_bid_rounds = max_bid_rounds
284
  self.attention_dropout = attention_dropout
285
  self.use_cache = use_cache
286
 
 
2471
  tokens_per_expert = _count_tokens_per_expert(flattened_selected_heads, num_experts)
2472
  max_count = tokens_per_expert.max().item()
2473
  no_overflow = max_count <= packed_length
2474
+ _enforce_no_overflow(no_overflow, max_count, tokens_per_expert)
2475
 
2476
  # -----------------------------------------------------------------------
2477
  # Construct the unpacking mask.
 
2589
  # Helpers
2590
  # ---------------------------------------------------------------------------
2591
 
2592
+ def _enforce_no_overflow(condition: bool, tokens_per_expert, max_length) -> None:
2593
  """Enforce that no expert bucket exceeds the preallocated packed length.
2594
 
2595
  This check fires when the number of tokens assigned to any expert in any
 
2615
  "Expert packing overflow: at least one expert bucket contains more "
2616
  "tokens than mosrah_packed_length allows. Increase "
2617
  "mosrah_overallocation_factor in ShramConfig to resolve."
2618
+ f"head lengths were: \n {tokens_per_expert}"
2619
+ f"max length was: {max_length}"
2620
  )
2621
 
2622
 
 
2812
  else:
2813
  self.capacity = config.mosrah_packed_length
2814
 
2815
+ self.max_bid_rounds = config.max_bid_rounds
2816
+
2817
  # W_r: routing projection, no bias (paper specifies xW_r, no additional term).
2818
  self.routing_projection = nn.Linear(
2819
  config.embedding_width, config.num_mosrah_heads, bias=False
 
2825
  self.expert_bias = nn.Parameter(torch.zeros(config.num_mosrah_heads))
2826
 
2827
  @staticmethod
2828
+ def get_threshold(
2829
+ tensor: torch.Tensor,
2830
+ dim: int,
2831
+ n: int | torch.Tensor,
2832
+ ) -> torch.Tensor:
2833
  """
2834
+ Returns the n-th largest value along dim, keepdim=True.
2835
+
2836
+ A value >= threshold ranks within the top n along dim. Boundary cases
2837
+ follow the monotone descending contract:
2838
+
2839
+ n == 0 -> +inf nothing qualifies
2840
+ n > dim_length -> -inf everything qualifies
2841
+
2842
+ :param tensor: Floating-point input, no NaN.
2843
+ :param dim: Dimension to reduce along.
2844
+ :param n: 1-indexed rank. Scalar int or tensor of ints broadcastable
2845
+ to tensor with dim removed.
2846
+ :return: Threshold with size 1 along dim, same dtype/device.
2847
  """
2848
+ # -------------------------------------------------------------------------
2849
+ # Algorithm overview
2850
+ # -------------------------------------------------------------------------
2851
+ #
2852
+ # Scalar n does not need a full sorted table. kthvalue selects the n-th
2853
+ # rank directly, and the two boundary sentinels are returned explicitly.
2854
+ #
2855
+ # Tensor n requires a full sorted table because each position along the
2856
+ # complementary dimensions may request a different rank. The table is
2857
+ # built once by sorting descending, then sentinel values are padded at
2858
+ # both ends so that boundary n values resolve correctly via gather:
2859
+ #
2860
+ # index 0 <- +inf sentinel (n == 0)
2861
+ # index 1..dim_length <- sorted values (valid ranks, 1-indexed)
2862
+ # index dim_length+1 <- -inf sentinel (n > dim_length)
2863
+ #
2864
+ # The critical invariant is that n is 1-indexed. This means valid ranks
2865
+ # map directly to their gather index without any offset, and index 0 is
2866
+ # naturally free for the +inf sentinel. n == 0 gathers +inf without
2867
+ # special-casing, and overflow n gathers -inf after clamping.
2868
+ #
2869
+ # F.pad specifies padding from the last dimension inward. Targeting an
2870
+ # arbitrary dim requires a positive index to compute how many trailing
2871
+ # dimensions to skip over in the pad spec.
2872
+ positive_dim = dim % tensor.ndim
2873
+ dim_length = tensor.shape[positive_dim]
2874
+
2875
+ if isinstance(n, int):
2876
+ # Scalar rank selection does not need a full sorted table. kthvalue
2877
+ # finds the k-th smallest; negating input and output flips the order
2878
+ # to give the k-th largest. Boundary sentinels follow the descending
2879
+ # contract: +inf sits above every real value (nothing qualifies),
2880
+ # -inf sits below every real value (everything qualifies).
2881
+ if n == 0:
2882
+ shape = list(tensor.shape)
2883
+ shape[positive_dim] = 1
2884
+ return tensor.new_full(shape, float('inf'))
2885
+ if n > dim_length:
2886
+ shape = list(tensor.shape)
2887
+ shape[positive_dim] = 1
2888
+ return tensor.new_full(shape, float('-inf'))
2889
+ return -torch.kthvalue(-tensor, n, dim=dim, keepdim=True).values
2890
+
2891
+ else:
2892
+ # Build the rank table once; each position gathers its own threshold.
2893
+ sorted_desc = torch.sort(tensor, dim=dim, descending=True).values
2894
+
2895
+ # Each trailing dimension after positive_dim contributes one (left,
2896
+ # right) zero-pair before the target padding entry in the F.pad spec.
2897
+ num_padding_skips = 2 * (tensor.ndim - positive_dim - 1)
2898
+ leading_pad = [0] * num_padding_skips + [1, 0]
2899
+ trailing_pad = [0] * num_padding_skips + [0, 1]
2900
+
2901
+ sorted_desc = F.pad(sorted_desc, leading_pad, value=float('inf'))
2902
+ sorted_desc = F.pad(sorted_desc, trailing_pad, value=float('-inf'))
2903
+
2904
+ # unsqueeze restores the reduced dimension so gather sees the same
2905
+ # rank as the padded table along dim.
2906
+ gather_index = n.clamp(0, dim_length + 1).long().unsqueeze(dim)
2907
+ return sorted_desc.gather(dim, gather_index)
2908
+ @staticmethod
2909
+ def _check_bidding_converged(converged: torch.Tensor, max_rounds: int) -> None:
2910
+ """Raise if the bidding loop exhausted max_rounds without satisfying all tokens.
2911
+
2912
+ In compiled mode ``torch._check`` fires a C++ assertion
2913
+ (``capture_scalar_outputs=True`` is a precondition β€” see Unit 19.F.1).
2914
+ In eager mode raises ``RuntimeError`` directly.
2915
+
2916
+ Exhausting ``max_rounds`` indicates an extreme routing density case or an
2917
+ infeasible configuration where total capacity is insufficient for N * K
2918
+ demands. In normal training this should never occur; the default
2919
+ ``max_bid_rounds=10`` covers approximately the 98th percentile of routing
2920
+ densities.
2921
+
2922
+ Args:
2923
+ converged: Scalar bool tensor β€” True if all tokens have >= K accepted experts.
2924
+ max_rounds: The iteration ceiling that was applied, for the error message.
2925
+ """
2926
+ if torch.compiler.is_compiling():
2927
+ torch._check(converged)
2928
+ else:
2929
+ if not converged.item():
2930
+ raise RuntimeError(
2931
+ f"balance_capacity bidding did not converge within {max_rounds} rounds. "
2932
+ f"All tokens must have at least K accepted experts before the loop exits. "
2933
+ f"This indicates either an infeasible configuration (total remaining "
2934
+ f"capacity < N * K) or an extreme routing density. "
2935
+ f"Increase mosrah_overallocation_factor or max_bid_rounds."
2936
+ )
2937
+
2938
+ @staticmethod
2939
+ def _run_bidding(
2940
+ logits: torch.Tensor,
2941
+ remaining_capacity: int | torch.Tensor,
2942
+ min_choices: int,
2943
+ max_rounds: int,
2944
+ ) -> torch.Tensor:
2945
+ """Deferred-acceptance (Gale-Shapley) bidding solver for joint capacity enforcement.
2946
+
2947
+ Tokens propose experts in descending preference order; experts provisionally
2948
+ accept their top-``remaining_capacity`` proposed tokens each round. Proposals
2949
+ are monotone (never retracted). The loop continues until every token has at
2950
+ least ``min_choices`` accepted experts or ``max_rounds`` is exhausted.
2951
 
2952
+ Both the column bound (per-expert token count ≀ remaining_capacity) and the
2953
+ row bound (per-token expert count β‰₯ min_choices) are satisfied simultaneously
2954
+ on the returned mask by construction.
2955
+
2956
+ Args:
2957
+ logits: Routing scores of shape (B, N, L).
2958
+ remaining_capacity: Per-expert token budget. Scalar int for training;
2959
+ (B, L) tensor for inference.
2960
+ min_choices: Minimum experts each token must have accepted (K).
2961
+ max_rounds: Iteration ceiling; raises via ``_check_bidding_converged``
2962
+ if exhausted.
2963
+
2964
+ Returns:
2965
+ accepted: (B, N, L) bool β€” True at positions accepted by the solver.
2966
+ """
2967
+ # ── initialise loop variables ─────────────────────────────────────────
2968
+ #
2969
+ # All three loop_vars must be tensors of fixed shape across iterations,
2970
+ # as required by torch.while_loop. logits and remaining_capacity are
2971
+ # captured read-only by the closures; they do not travel as loop_vars.
2972
+ proposals = torch.zeros_like(logits, dtype=torch.bool)
2973
+ acceptances = torch.zeros_like(logits, dtype=torch.bool)
2974
+ round_count = torch.zeros((), device=logits.device, dtype=torch.int64)
2975
+ max_rounds_t = torch.full((), max_rounds, device=logits.device, dtype=torch.int64)
2976
+
2977
+ def cond_fn(proposals, acceptances, round_count):
2978
+ all_satisfied = (acceptances.sum(dim=-1) >= min_choices).all()
2979
+ return (round_count < max_rounds_t) & ~all_satisfied
2980
+
2981
+ def body_fn(proposals, acceptances, round_count):
2982
+ # ── token proposal step ───────────────────────────────────────────
2983
+ #
2984
+ # Tokens with fewer than min_choices accepted experts propose their
2985
+ # next-best unproposed expert(s). The deficit determines how many new
2986
+ # proposals each token makes this round; already-satisfied tokens
2987
+ # propose nothing (deficit = 0 β†’ bid_threshold = +inf β†’ no new bids).
2988
+ accepted_per_token = acceptances.sum(dim=-1) # (B, N)
2989
+ choices_deficit = (min_choices - accepted_per_token).clamp_min(0)
2990
+
2991
+ unproposed_logits = logits.masked_fill(proposals, float('-inf'))
2992
+ bid_threshold = MoSRAHRouter.get_threshold(
2993
+ unproposed_logits, dim=-1, n=choices_deficit,
2994
+ )
2995
+ new_proposals = (
2996
+ (unproposed_logits >= bid_threshold)
2997
+ & ~proposals
2998
+ & (choices_deficit.unsqueeze(-1) > 0)
2999
+ )
3000
+ updated_proposals = proposals | new_proposals
3001
+
3002
+ # ── expert acceptance step ────────────────────────────────────────
3003
+ #
3004
+ # Each expert accepts its top-remaining_capacity proposed tokens.
3005
+ # Acceptances are recomputed from scratch each round so that a
3006
+ # stronger new proposal can displace a weaker prior one.
3007
+ proposed_logits = logits.masked_fill(~updated_proposals, float('-inf'))
3008
+ accept_threshold = MoSRAHRouter.get_threshold(
3009
+ proposed_logits, dim=-2, n=remaining_capacity,
3010
+ )
3011
+ updated_acceptances = updated_proposals & (proposed_logits >= accept_threshold)
3012
+
3013
+ return updated_proposals, updated_acceptances, round_count + 1
3014
+
3015
+ proposals, acceptances, _ = torch.while_loop(
3016
+ cond_fn, body_fn, (proposals, acceptances, round_count),
3017
+ )
3018
+
3019
+ converged = (acceptances.sum(dim=-1) >= min_choices).all()
3020
+ MoSRAHRouter._check_bidding_converged(converged, max_rounds)
3021
+ return acceptances
3022
+
3023
+ @classmethod
3024
+ def balance_capacity(
3025
+ cls,
3026
+ logits: torch.Tensor,
3027
+ used_capacity: torch.Tensor | None,
3028
+ capacity: int,
3029
+ min_choices: int,
3030
+ max_rounds: int,
3031
+ mask_value: float = -1e8,
3032
+ ) -> torch.Tensor:
3033
+ """Mask logits so both capacity constraints hold simultaneously on the output.
3034
+
3035
+ Two constraints must hold:
3036
+ - Column bound: per-expert unmasked token count ≀ remaining_capacity.
3037
+ - Row bound: per-token unmasked expert count β‰₯ min_choices.
3038
+
3039
+ A training fast path and a column-capacity fast path are attempted before
3040
+ falling back to the bidding solver:
3041
+
3042
+ 1. Training with N ≀ capacity: return logits unchanged.
3043
+ 2. Column-capacity fast path: if the most permissive column-bound-satisfying
3044
+ mask already gives every token at least min_choices choices, return it.
3045
+ 3. Bidding fallback: deferred-acceptance solver guaranteeing both bounds.
3046
+
3047
+ Args:
3048
+ logits: Routing scores of shape (B, N, L).
3049
+ used_capacity: Tokens already accumulated per expert, shape (B, L).
3050
+ ``None`` during training (full capacity available).
3051
+ capacity: Maximum tokens per expert (from config).
3052
+ min_choices: Minimum experts each token must retain (K).
3053
+ max_rounds: Bidding iteration ceiling (from config.max_bid_rounds).
3054
+ mask_value: Value written to masked positions. Default -1e8.
3055
+
3056
+ Returns:
3057
+ Logits with unavailable positions set to ``mask_value``, shape (B, N, L).
3058
+ """
3059
+ # ── Algorithm overview ────────────────────────────────────────────────
3060
+ #
3061
+ # Problem: mask (B, N, L) logits so that both the column bound (each
3062
+ # expert receives at most remaining_capacity tokens) and the row bound
3063
+ # (each token retains at least min_choices expert choices) hold
3064
+ # simultaneously. Satisfying either constraint greedily can violate the
3065
+ # other, requiring a joint solver for the hard case.
3066
+ #
3067
+ # Approach: deferred-acceptance (Gale-Shapley) bidding. Each round,
3068
+ # tokens that still lack min_choices accepted experts propose their
3069
+ # next-best unproposed expert. Each expert then provisionally accepts its
3070
+ # top-remaining_capacity proposed tokens, potentially displacing weaker
3071
+ # prior acceptances. Proposals are monotone (never retracted). The loop
3072
+ # terminates when every token has min_choices accepted experts or
3073
+ # max_bid_rounds is exhausted (RuntimeError in the latter case).
3074
+ #
3075
+ # Two cheaper paths precede the solver:
3076
+ #
3077
+ # Training fast path β€” when N ≀ capacity and all experts start empty,
3078
+ # no expert can overflow regardless of routing. No masking is needed.
3079
+ #
3080
+ # Column-capacity fast path β€” the most permissive mask satisfying the
3081
+ # column bound selects each expert's top-remaining_capacity tokens. If
3082
+ # that mask also satisfies the row bound, both constraints hold and the
3083
+ # solver is skipped entirely.
3084
+
3085
+ # Training fast path: N ≀ capacity with empty experts β†’ no overflow possible.
3086
+ if used_capacity is None and logits.shape[-2] <= capacity:
3087
+ return logits
3088
+
3089
+ # Compute per-expert remaining budget.
3090
+ # Training (N > capacity path): scalar β€” all experts start with full capacity.
3091
+ # Inference: subtract already-accumulated tokens; clamp prevents negatives
3092
+ # when rounding causes used_capacity to slightly exceed capacity.
3093
  if used_capacity is None:
3094
+ remaining_capacity = capacity
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3095
  else:
3096
+ remaining_capacity = (capacity - used_capacity).clamp(min=0) # (B, L)
3097
+
3098
+ # Column-capacity fast path: select each expert's top-remaining_capacity
3099
+ # tokens β€” the most permissive mask satisfying the column bound. If it
3100
+ # also satisfies the row bound, both constraints hold simultaneously.
3101
+ # Mask computation runs under no_grad: the boolean mask is a hard routing
3102
+ # decision and must not accumulate gradient memory through the solver.
3103
+ with torch.no_grad():
3104
+ col_threshold = cls.get_threshold(logits, dim=-2, n=remaining_capacity)
3105
+ col_capacity_mask = logits >= col_threshold # (B, N, L)
3106
+ if (col_capacity_mask.sum(dim=-1) >= min_choices).all():
3107
+ return logits.masked_fill(~col_capacity_mask, mask_value)
3108
+
3109
+ # Column-capacity mask violates the row bound: routing is concentrated
3110
+ # enough that per-expert capacity limits leave some tokens with fewer
3111
+ # than min_choices choices. The bidding solver handles this jointly.
3112
+ with torch.no_grad():
3113
+ accepted = cls._run_bidding(logits, remaining_capacity, min_choices, max_rounds)
3114
+ return logits.masked_fill(~accepted, mask_value)
 
 
 
 
 
 
 
3115
  def forward(
3116
  self,
3117
  x: torch.Tensor,
 
3151
  # selection. expert_bias is added to logits before softmax so that the bias
3152
  # shifts selection probability without rescaling the unbiased distribution.
3153
  biased_logits = logits + self.expert_bias
3154
+ biased_logits = self.balance_capacity(
3155
+ biased_logits,
3156
+ used_capacity,
3157
+ self.capacity,
3158
+ self.num_selected_heads,
3159
+ self.max_bid_rounds,
3160
+ )
3161
  biased_routing_scores = F.softmax( # RΜ‚, (B, N, L)
3162
  biased_logits, dim=-1
3163
  )