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

"""
MoE Specialization Callback
============================
Monitors whether MoE experts are developing distinct, stable roles over training.
A well-trained MoE should have experts that specialize β€” each processing a different
kind of input β€” rather than a few generalist experts doing everything while the rest
idle.

  Expert Co-activation Rate
  -------------------------
  If two experts frequently fire together on the same token (both in the top-K
  selected), they are likely learning redundant representations. Ideally experts
  specialize on non-overlapping token types, so co-activation should stay close
  to the chance baseline of K/N (e.g. 8/128 β‰ˆ 0.0625 for the 235B model).

  For each layer and each unique expert pair (i, j), we compute:
      CoAct(i, j) = N_{i,j} / N_i
  where N_{i,j} = number of tokens where both i and j were selected, and N_i =
  total tokens routed to expert i. We then summarize across all pairs as max and
  mean. A rising mean_coact, especially well above the chance baseline, signals
  that the router is collapsing onto a small correlated cluster of experts.

Buffer ownership
----------------
  coactivation_counts is reset here (in compute_moe_coactivation_metrics).
  Per-expert token counts are derived from coactivation_counts itself
  (row_sum + col_sum) / (K-1), so this callback is fully independent of
  ExpertHeatmap's reset cycle for total_tokens_per_expert.
"""

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 _get_device_mesh(vfm: torch.nn.Module):
    weight = vfm.language_model.model.layers[0].self_attn.q_proj.weight
    return weight.device_mesh if isinstance(weight, DTensor) else None


def _allreduce_dtensor(t: torch.Tensor, device_mesh) -> torch.Tensor:
    """Sum-reduce a local tensor across all FSDP ranks and return the global tensor."""
    return DTensor.from_local(
        t,
        device_mesh=device_mesh,
        placements=[Partial()] * device_mesh.ndim,
    ).full_tensor()


def compute_moe_coactivation_metrics(vfm: torch.nn.Module) -> dict[str, dict]:
    """
    Compute per-layer Expert Co-activation metrics for both towers.

    For each unique expert pair (i < j) in the upper triangle of the NΓ—N
    coactivation matrix, computes:
        CoAct(i, j) = N_{i,j} / N_i
    where N_{i,j} is the count of tokens where both i and j were in the top-K,
    and N_i is the total token count for expert i (the row expert, i.e. the
    lower-indexed expert in the pair).

    N_i is derived directly from the co-activation matrix rather than from
    the shared total_tokens_per_expert buffer, so this metric is independent
    of ExpertHeatmap's reset cycle.  Each token routed to expert i contributes
    to (K-1) co-activation pairs, so N_i = (row_sum_i + col_sum_i) / (K-1).

    High co-activation relative to the chance baseline (K/N) indicates that
    certain expert pairs are systematically selected together β€” a sign of
    redundancy rather than specialization.

    Returns a dict: tower -> {
        "layer_indices":  list[int]          β€” actual model layer positions
        "max_coact":      Tensor[num_moe_layers]  β€” worst pair per layer
        "mean_coact":     Tensor[num_moe_layers]  β€” average over all pairs
        "chance_baseline": float             β€” K/N, same for all layers (reference)
    }
    """
    with torch.no_grad():
        device_mesh = _get_device_mesh(vfm)
        if device_mesh is None:
            return {}

        results: dict[str, dict] = {}
        for tower in ["und", "gen"]:
            layer_indices, max_coacts, mean_coacts, chance_baselines = [], [], [], []

            num_layers = len(vfm.language_model.model.layers)
            for layer_idx in range(num_layers):
                layer = vfm.language_model.model.layers[layer_idx]
                mlp = layer.mlp if tower == "und" else getattr(layer, "mlp_moe_gen", None)
                if not isinstance(mlp, Qwen3VLMoeTextSparseMoeBlock):
                    continue

                coact_counts = _allreduce_dtensor(mlp.get_coactivation_counts(reset=True), device_mesh)  # [N, N]

                n = mlp.num_experts
                k = mlp.top_k

                # Derive per-expert token counts directly from the co-activation
                # matrix so we don't depend on ExpertHeatmap's reset cycle.
                # Each token that routes to expert i contributes (K-1) entries
                # across row i and column i of the upper-triangle matrix.
                tokens_per_expert = (coact_counts.sum(dim=1) + coact_counts.sum(dim=0)).float() / (k - 1)

                mask = torch.triu(torch.ones(n, n, dtype=torch.bool, device=coact_counts.device), diagonal=1)
                # CoAct(i, j) = N_{i,j} / N_i β€” normalise by how often expert i fires overall.
                denom = tokens_per_expert.unsqueeze(1).clamp(min=1)  # [N, 1]
                coact_rates = (coact_counts.float() / denom)[mask]  # [N*(N-1)/2]

                layer_indices.append(layer_idx)
                max_coacts.append(coact_rates.max())
                mean_coacts.append(coact_rates.mean())
                # Chance baseline = probability two randomly-chosen top-K slots land on the
                # same pair under uniform routing = K/N. Constant across layers and steps,
                # logged once per tower as a reference line.
                chance_baselines.append(k / n)

            if layer_indices:
                results[tower] = {
                    "layer_indices": layer_indices,
                    "max_coact": torch.stack(max_coacts),
                    "mean_coact": torch.stack(mean_coacts),
                    "chance_baseline": chance_baselines[0],  # same value for all layers
                }

    return results


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

    What it captures
    ----------------
    Whether MoE experts are developing distinct routing identities:

    Expert Co-activation (logged every N steps)
      - mean_coact / max_coact per layer: how often expert pairs fire together
        relative to the chance_baseline (K/N). Values well above the baseline
        suggest the router is selecting a redundant cluster of experts rather
        than a diverse set.

    W&B layout
    ----------
    moe_specialization/coact_chance_baseline/<tower>         β€” flat reference (K/N)
    moe_specialization/max_coact/<tower>/layer_NNN|mean|max
    moe_specialization/mean_coact/<tower>/layer_NNN|mean|max

    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:
        vfm = model.net

        coact_results = compute_moe_coactivation_metrics(vfm)

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

        log_dict: dict[str, float] = {}

        for tower, tower_metrics in coact_results.items():
            layer_indices = tower_metrics.pop("layer_indices")
            chance_baseline = tower_metrics.pop("chance_baseline")
            log_dict[f"moe_specialization/coact_chance_baseline/{tower}"] = chance_baseline
            for metric_name, values in tower_metrics.items():
                for layer_idx, val in zip(layer_indices, values):
                    log_dict[f"moe_specialization/{metric_name}/{tower}/layer_{layer_idx:03d}"] = val.item()
                log_dict[f"moe_specialization/{metric_name}/{tower}/mean"] = values.mean().item()
                log_dict[f"moe_specialization/{metric_name}/{tower}/max"] = values.max().item()

        wandb.log(log_dict, step=iteration)