File size: 14,613 Bytes
9f818c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: OpenMDW-1.1

"""
MoE Stability Callback
======================
Monitors whether the MoE router is staying healthy over the course of training.
A healthy router distributes tokens reasonably evenly, keeps all experts alive,
and remains uncertain enough (high entropy) that it is still learning to route.

Five metrics are tracked per layer, per tower (und / gen):

  Dead Expert Rate
  ----------------
  Fraction of experts receiving fewer than 10% of their fair-share of tokens
  (i.e. load fraction f_i < 0.1 / N). A dead expert has been effectively shut
  out by the router β€” it gets no gradient signal and its capacity is wasted.
  Ideal = 0. A rising dead-expert rate in the gen tower during early training
  is a common failure mode.

  Load Imbalance Factor (LIF)
  ---------------------------
  N * max(f_i), where f_i is the fraction of tokens routed to expert i.
  Measures how much the busiest expert is overloaded relative to uniform.
  LIF = 1.0 is perfect balance; <= 1.3 is healthy; > 3.0 indicates severe
  collapse onto a small set of experts. This is the same quantity watched by
  the load-balancing loss, but measured empirically rather than from the loss
  objective.

  Router Entropy (normalized)
  ---------------------------
  Mean per-token Shannon entropy of the full routing distribution, divided by
  log(N) to put it on a [0, 1] scale. H = 1 means the router is maximally
  uncertain (uniform over all experts); H = 0 means it always picks the same
  expert. Early in training entropy is high; we want it to stay reasonably
  high (> ~0.7) so the router continues to explore. A sudden drop signals
  routing collapse.

  Soft-vs-Hard Effective Experts (normalized)
  -------------------------------------------
  Soft and hard effective experts separate what the router *considers* (full
  probability distribution, before dispatch) from what top-k dispatch *actually
  uses* (empirical token-to-expert assignment, after dispatch). Both are
  expressed as a fraction of N, so they sit on the same axis as
  router_entropy_normalized. Their lower bounds differ slightly:
    soft_eff_normalized is bounded in [1/N, 1].
    hard_eff_normalized is bounded in [K/N, 1] β€” top-K dispatch always engages
      at least K experts in aggregate (the floor case is when every token
      picks the same K-expert subset).

      soft_eff_normalized = mean_t exp(H(p_t)) / N
        Average per-token router perplexity, divided by N. Asks: what fraction
        of experts is the router *considering* on a typical token? Computed
        as sum_per_token_soft_eff / total_tokens / N. Note: the unnormalized
        numerator is NOT exp of the mean entropy β€” by Jensen,
        mean_t exp(H_t) >= exp(mean_t H_t), and the gap matters when
        per-token entropies are heterogeneous.

      hard_eff_normalized = exp(H(f)) / N
        where f_i is the empirical fraction of *expert assignments* (not
        tokens) that went to expert i: f_i = tokens_per_expert_i / (T * K).
        Perplexity of the buffer-wide dispatch distribution, divided by N.
        Asks: what fraction of experts is top-k *actually* engaging across the
        buffer? A smoother sibling of LIF: where LIF watches the busiest
        expert, hard_eff watches the spread of the whole load distribution.

  Interpretation (high/low refer to values close to 1 vs close to 1/N):

      high soft_eff, high hard_eff
        Router considers many experts; top-k dispatch also uses many experts.
        Broadly healthy routing.
      low  soft_eff, low  hard_eff
        Router is confident or collapsed in probability space; dispatch is
        also concentrated. Entropy, LIF, and hard usage all agree that
        routing is narrow.
      high soft_eff, low  hard_eff
        Router distribution is broad, but top-k dispatch is concentrated β€”
        the "hidden top-k concentration" case where entropy can look healthy
        while LIF and co-activation are high.
      low  soft_eff, high hard_eff
        Less common: each token has a sharp router distribution, but
        different tokens choose different experts. Per-token confidence with
        buffer-wide diversity.

Buffer ownership
----------------
  This callback is fully self-contained: it reads and resets its own dedicated
  buffers (stability_tokens_per_expert, stability_total_tokens, sum_token_entropy,
  sum_per_token_soft_eff). It does not depend on ExpertHeatmap's reset cycle.
"""

import math

# Fraction of uniform fair-share below which an expert is considered "dead" (e.g. 0.1 β†’ < 10% of K/N).
DEAD_EXPERT_THRESHOLD_MULTIPLIER = 0.1

# Smoothing added inside log() to avoid log(0) for experts that received zero
# tokens in the current buffer window. Matches the constant used inside the
# MoE block when accumulating router entropy.
ENTROPY_EPSILON = 1e-9

import torch
import wandb
from torch.distributed.tensor import DTensor, Partial

from cosmos_framework.callbacks.every_n import EveryN
from cosmos_framework.model._base import ImaginaireModel
from cosmos_framework.trainer import ImaginaireTrainer
from cosmos_framework.utils import distributed
from cosmos_framework.model.vfm.vlm.qwen3_vl_moe.qwen3_vl_moe import Qwen3VLMoeTextSparseMoeBlock


def _effective_experts(
    sum_per_token_soft_eff: torch.Tensor,
    total_tokens: torch.Tensor,
    tokens_per_expert: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
    """Compute (soft_eff, hard_eff) from already-reduced stability buffers.

    Extracted as a pure-tensor function so it can be unit-tested without
    instantiating any MoE module or distributed state.

    Args:
        sum_per_token_soft_eff: 0-d or [1] tensor holding sum_t exp(H(p_t))
            accumulated across the buffer window.
        total_tokens: 0-d or [1] tensor holding the number of tokens seen
            since the last reset.
        tokens_per_expert: [N] tensor of per-expert token counts over the
            same buffer window.

    Returns:
        soft_eff: scalar tensor, mean_t exp(H(p_t)) in [1, N].
        hard_eff: scalar tensor, exp(H(f)) over the empirical dispatch
            distribution f_i = tokens_per_expert_i / sum_i tokens_per_expert_i.
            Bounded in [K, N] (not [1, N]) because top-K dispatch always
            engages at least K experts in aggregate.

    Note on hard_eff normalization:
        tokens_per_expert is a histogram over the K top-k slots per token, so
        it sums to T * K rather than T. We must divide by its own sum (== T*K)
        to get a true probability distribution before taking entropy.
        Dividing by total_tokens (== T) instead would give a vector summing to
        K, producing exp(H) values up to (N/K)^K β€” orders of magnitude beyond
        the intended [K, N] range.
    """
    total = total_tokens.float().clamp(min=1)
    soft_eff = (sum_per_token_soft_eff.float() / total).squeeze()

    total_assignments = tokens_per_expert.sum().float().clamp(min=1)
    f_i = (tokens_per_expert.float() / total_assignments).clamp(min=ENTROPY_EPSILON)
    hard_entropy = -(f_i * f_i.log()).sum()
    hard_eff = hard_entropy.exp()

    return soft_eff, hard_eff


def compute_moe_stability_metrics(vfm: torch.nn.Module) -> dict[str, dict]:
    """
    Compute per-layer MoE stability metrics for both towers.

    Iterates over all model layers, skipping any that do not use
    Qwen3VLMoeTextSparseMoeBlock (e.g. dense layers when decoder_sparse_step > 1).
    Actual model layer indices are preserved so W&B keys (layer_000, layer_042, ...)
    always refer to the correct transformer layer regardless of MoE sparsity pattern.

    Returns a dict: tower -> {
        "layer_indices":             list[int]                β€” actual model layer positions
        "dead_expert_rate":          Tensor[num_moe_layers]
        "lif":                       Tensor[num_moe_layers]
        "router_entropy_normalized": Tensor[num_moe_layers]
        "soft_eff_normalized":       Tensor[num_moe_layers]   β€” mean_t exp(H(p_t)) / N,  in [1/N, 1]
        "hard_eff_normalized":       Tensor[num_moe_layers]   β€” exp(H(f)) / N,           in [1/N, 1]
    }
    """
    with torch.no_grad():
        num_layers = len(vfm.language_model.model.layers)

        example_weight = vfm.language_model.model.layers[0].self_attn.q_proj.weight
        device_mesh = example_weight.device_mesh if isinstance(example_weight, DTensor) else None

        if device_mesh is None:
            return {}

        def _allreduce(t: torch.Tensor) -> torch.Tensor:
            return DTensor.from_local(
                t,
                device_mesh=device_mesh,
                placements=[Partial()] * device_mesh.ndim,
            ).full_tensor()

        results: dict[str, dict] = {}
        for tower in ["und", "gen"]:
            layer_indices: list[int] = []
            dead_rates: list[torch.Tensor] = []
            lifs: list[torch.Tensor] = []
            entropies: list[torch.Tensor] = []
            soft_effs_norm: list[torch.Tensor] = []
            hard_effs_norm: list[torch.Tensor] = []

            for layer_idx in range(num_layers):
                layer_module = vfm.language_model.model.layers[layer_idx]
                # "und" tower uses layer.mlp; "gen" tower uses layer.mlp_moe_gen.
                # Both attributes exist on every layer (set in unified_mot.py), but only
                # layers where (layer_idx+1) % decoder_sparse_step == 0 are MoE blocks.
                mlp_module = layer_module.mlp if tower == "und" else getattr(layer_module, "mlp_moe_gen", None)
                if not isinstance(mlp_module, Qwen3VLMoeTextSparseMoeBlock):
                    continue

                total_tokens_per_expert = _allreduce(mlp_module.get_stability_tokens_per_expert(reset=True))
                total_tokens = _allreduce(mlp_module.get_stability_total_tokens(reset=True))
                sum_token_entropy = _allreduce(mlp_module.get_sum_token_entropy(reset=True))
                sum_per_token_soft_eff = _allreduce(mlp_module.get_sum_per_token_soft_eff(reset=True))

                n = mlp_module.num_experts
                total = total_tokens.float().clamp(min=1)
                f_i = total_tokens_per_expert.float() / total  # [N] load fraction per expert

                k = mlp_module.top_k

                layer_indices.append(layer_idx)
                # Uniform fair share per expert is K/N.  "Dead" = below 10% of that.
                dead_rates.append((f_i < DEAD_EXPERT_THRESHOLD_MULTIPLIER * k / n).float().mean())
                # LIF = max(f_i) * N / K.  Interpretation:
                #   1.0 = perfectly balanced (every expert gets its fair share)
                #   2.0 = busiest expert handles 2x its fair share
                #   >3.0 = severe imbalance, consider tuning load-balancing loss
                lifs.append(f_i.max() * n / k)
                # Mean per-token entropy, normalized to [0, 1] by log(N).
                # squeeze() collapses the [1] buffer shape to a 0-d scalar.
                entropies.append((sum_token_entropy.float() / total / math.log(n)).squeeze())

                soft_eff, hard_eff = _effective_experts(
                    sum_per_token_soft_eff=sum_per_token_soft_eff,
                    total_tokens=total_tokens,
                    tokens_per_expert=total_tokens_per_expert,
                )
                soft_effs_norm.append(soft_eff / n)
                hard_effs_norm.append(hard_eff / n)

            if layer_indices:
                results[tower] = {
                    "layer_indices": layer_indices,
                    "dead_expert_rate": torch.stack(dead_rates),
                    "lif": torch.stack(lifs),
                    "router_entropy_normalized": torch.stack(entropies),
                    "soft_eff_normalized": torch.stack(soft_effs_norm),
                    "hard_eff_normalized": torch.stack(hard_effs_norm),
                }

    return results


class MoEStabilityCallback(EveryN):
    """
    Logs per-layer MoE stability metrics to W&B every N training steps.

    What it captures
    ----------------
    Whether the MoE router remains in a healthy, balanced state over training.
    The metrics collectively answer: are all experts still being used
    (dead_expert_rate), is load spread evenly (lif), is the router still
    making uncertain, exploratory decisions (router_entropy_normalized), and
    do the experts the router considers (soft_eff) match the experts top-k
    dispatch actually engages (hard_eff)?

    W&B layout
    ----------
    For each metric and each tower, two kinds of series are logged:
      - moe_stability/<metric>/<tower>/layer_NNN  β€” per model layer time series
      - moe_stability/<metric>/<tower>/mean|max   β€” summary across all MoE layers

    Metrics logged: dead_expert_rate, lif, router_entropy_normalized,
    soft_eff_normalized, hard_eff_normalized.

    Typical healthy ranges:
      dead_expert_rate  β†’ 0 (any sustained non-zero value is a concern)
      lif               β†’ <= 1.3 (alarm at > 3.0)
      router_entropy_normalized β†’ > 0.7 (collapse if it drops sharply)
      soft_eff_normalized, hard_eff_normalized β†’ high; a large gap between
        them (e.g. soft high, hard low) indicates hidden top-k concentration

    Args:
        every_n (int): Logging interval in training steps.
    """

    def __init__(self, every_n: int = 100):
        super().__init__(every_n=every_n)

    def every_n_impl(
        self,
        trainer: ImaginaireTrainer,
        model: ImaginaireModel,
        data_batch: dict[str, torch.Tensor],
        output_batch: dict[str, torch.Tensor],
        loss: torch.Tensor,
        iteration: int,
    ) -> None:
        metrics = compute_moe_stability_metrics(model.net)

        if not (distributed.is_rank0() and wandb.run):
            return

        log_dict: dict[str, float] = {}
        for tower, tower_metrics in metrics.items():
            layer_indices = tower_metrics.pop("layer_indices")
            for metric_name, values in tower_metrics.items():
                for layer_idx, val in zip(layer_indices, values):
                    log_dict[f"moe_stability/{metric_name}/{tower}/layer_{layer_idx:03d}"] = val.item()
                log_dict[f"moe_stability/{metric_name}/{tower}/mean"] = values.mean().item()
                log_dict[f"moe_stability/{metric_name}/{tower}/max"] = values.max().item()

        wandb.log(log_dict, step=iteration)