Directory Diff Report

Left: experiments/grug/base
Right: experiments/grug/moe_pipeline
Filter: .py, .md
Generated: 2026-09-08 20:33:26Z
Inline Diffs: 6 files
Changed
2
Added
3
Removed
1
Unchanged
1
File Status Lines Added Lines Deleted
model.pychanged+272-163
train.pychanged+377-633
README.mdadded+61-0
benchmark.pyadded+99-0
pipeline.pyadded+624-0
launch.pyremoved+0-263
__init__.pyunchanged+0-0

model.py

changed

experiments/grug/base/model.py
experiments/grug/moe_pipeline/model.py
f1# Copyright The Marin Authorsf1# Copyright The Marin Authors
2# SPDX-License-Identifier: Apache-2.02# SPDX-License-Identifier: Apache-2.0
33
nn4"""MoE grug variant model.
5 
6Architecture: QB-routed MoE with GatedNorm, XSA, sigmoid combine weights.
7No load-balancing loss; router z-loss only. All layers are MoE (no dense layers).
8"""
9 
4import dataclasses10import dataclasses
5from dataclasses import dataclass11from dataclasses import dataclass
nn12from typing import Any, Literal
613
7import equinox as eqx14import equinox as eqx
8import jax15import jax
9import jax.numpy as jnp16import jax.numpy as jnp
n10from einops import rearrangen17from haliax import Axis
11from haliax.jax_utils import named_call18from haliax.jax_utils import named_call
12from jax import random19from jax import random
n13from jax.sharding import PartitionSpec as Pn
14from jax.sharding import reshard20from jax.sharding import reshard
15from jaxtyping import Array, Float, Int, PRNGKeyArray21from jaxtyping import Array, Float, Int, PRNGKeyArray
n16from levanter.analysis.backward_flow import (n22from levanter.compat.hf_checkpoints import HFCheckpointConverter
17    is_backward_flow_active,23from levanter.grug.attention import (
18    log_backward_activation,24    AttentionMask,
19    trace_backward_activation,25    GrugAttentionImplementation,
20    trace_grads,26    RotaryConfig,
21)27)
n22from levanter.grug.attention import AttentionMask, RotaryConfig, apply_rotary_embedding, attentionn28from levanter.grug.grug_moe import (
29    MOE_REMAT_SAVE_NAMES,
30    MoeActivation,
31    MoeImplementation,
32    resolve_moe_implementation,
33)
23from levanter.grug.loss import fused_linear_softmax_cross_entropy_loss34from levanter.grug.loss import fused_linear_softmax_cross_entropy_loss
n24from levanter.grug.sharding import Pbatch, Pembed_vocab, Plm_head, Plogits, unshardn35from levanter.grug.sharding import Pembed_vocab, Plm_head
36from levanter.tracker.histogram import SummaryStats
37from transformers import PretrainedConfig as HfConfig
38 
39from experiments.grug.moe.model import _BATCH_AXES as BATCH_AXES  # noqa: F401
40from experiments.grug.moe.model import GRUG_MOE_ARCHITECTURE as GRUG_MOE_ARCHITECTURE
41from experiments.grug.moe.model import GRUG_MOE_ARTIFACT_SCHEMA_VERSION as GRUG_MOE_ARTIFACT_SCHEMA_VERSION
42from experiments.grug.moe.model import (
43    GRUG_MOE_ARTIFACT_SCHEMA_VERSION_KEY as GRUG_MOE_ARTIFACT_SCHEMA_VERSION_KEY,
44)
45from experiments.grug.moe.model import GRUG_MOE_MODEL_TYPE as GRUG_MOE_MODEL_TYPE
46from experiments.grug.moe.model import Block as Block
47from experiments.grug.moe.model import CausalSelfAttention as CausalSelfAttention
48from experiments.grug.moe.model import DenseMLP as DenseMLP
49from experiments.grug.moe.model import GatedNorm as GatedNorm
50from experiments.grug.moe.model import GrugMoeHfConfig as GrugMoeHfConfig
51from experiments.grug.moe.model import MoEMLP as MoEMLP
52from experiments.grug.moe.model import RMSNorm as RMSNorm
53from experiments.grug.moe.model import _batch_spec as _batch_spec
54from experiments.grug.moe.model import _hf_config_attr as _hf_config_attr
55from experiments.grug.moe.model import _init_weight as _init_weight
56from experiments.grug.moe.model import _summarize_router_metrics as _summarize_router_metrics
57from experiments.grug.moe.model import debug_mesh_and_token_pspec as debug_mesh_and_token_pspec
58from experiments.grug.moe.model import grugmoe_inference_state_dict as grugmoe_inference_state_dict
59 
60RematMode = Literal["recompute_all", "save_moe"]
2561
2662
27@dataclass(frozen=True)63@dataclass(frozen=True)
28class GrugModelConfig:64class GrugModelConfig:
n29    """Hyperparameters for the Grug Llama-style transformer."""n65    """Architecture and execution settings for the Grug MoE transformer."""
3066
31    vocab_size: int67    vocab_size: int
n32    hidden_dim: int = 2048n68    hidden_dim: int = 512
33    intermediate_dim: int = 563269    intermediate_dim: int = 256
70    shared_expert_intermediate_dim: int = 512
71    num_experts: int = 256
72    num_experts_per_token: int = 4
34    num_layers: int = 2473    num_layers: int = 6
35    num_heads: int = 1674    num_heads: int = 4
36    num_kv_heads: int = 1675    num_kv_heads: int = 1
37    head_dim: int | None = None76    head_dim: int | None = None
n38    max_seq_len: int = 4096n77    max_seq_len: int = 8192
78    sliding_window: int = 2048
39    layer_norm_eps: float = 1e-579    layer_norm_eps: float = 1e-5
40    initializer_std: float = 0.0280    initializer_std: float = 0.02
nn81    qk_mult: float = 1.3
82    router_z_loss_coef: float = 0.0
83    disable_pko: bool = True
84    """When True (default), the every-4th + last 'long' layers skip Partial
85    Key Offset (no shift of the second half of K, no doc-start zeroing). Short
86    layers never had PKO. Set to False to re-enable PKO on long layers."""
87    disable_long_rope: bool = True
88    """When True (default), the every-4th + last 'long' layers skip rotary
89    embedding entirely (Q and K go into attention un-rotated). Short layers
90    still apply half-RoPE. Set to False to keep RoPE on long layers."""
91    attention_implementation: GrugAttentionImplementation | None = None
92    moe_implementation: MoeImplementation | None = None
93    capacity_factor: float = 1.0
94    remat_mode: RematMode = "recompute_all"
95    """Per-block gradient checkpointing. "recompute_all" reruns the whole block in
96    backward (lowest memory); "save_moe" keeps the tagged MoE dispatch tensors so
97    backward skips re-running expert dispatch and its EP collectives."""
41    rope: RotaryConfig = dataclasses.field(default_factory=RotaryConfig)98    rope: RotaryConfig = dataclasses.field(default_factory=RotaryConfig)
4299
43    def __post_init__(self) -> None:100    def __post_init__(self) -> None:
48            raise ValueError("vocab_size must be positive")105            raise ValueError("vocab_size must be positive")
49        if self.max_seq_len <= 0:106        if self.max_seq_len <= 0:
50            raise ValueError("max_seq_len must be positive")107            raise ValueError("max_seq_len must be positive")
nn108        if self.num_experts <= 0:
109            raise ValueError("num_experts must be positive")
110        if self.num_experts_per_token <= 0:
111            raise ValueError("num_experts_per_token must be positive")
112        if self.num_experts_per_token > self.num_experts:
113            raise ValueError("num_experts_per_token must be <= num_experts")
114        if self.shared_expert_intermediate_dim < 0:
115            raise ValueError("shared_expert_intermediate_dim must be non-negative")
116        if self.capacity_factor <= 0:
117            raise ValueError("capacity_factor must be positive")
118        resolve_moe_implementation(self.moe_implementation)
119 
120    @property
121    def Embed(self) -> Axis:
122        return Axis("embed", self.hidden_dim)
123 
124    @property
125    def model_type(self) -> type["Transformer"]:
126        return Transformer
51127
52    @property128    @property
53    def inferred_head_dim(self) -> int:129    def inferred_head_dim(self) -> int:
59            )135            )
60        return self.hidden_dim // self.num_heads136        return self.hidden_dim // self.num_heads
61137
nn138    def build(self, Vocab: Axis, *, key: PRNGKeyArray) -> "Transformer":
139        cfg = self if Vocab.size == self.vocab_size else dataclasses.replace(self, vocab_size=Vocab.size)
140        return Transformer.init(cfg, key=key)
62141
n63class CausalSelfAttention(eqx.Module):n142    def hf_checkpoint_converter(
64    w_q: jax.Array143        self,
65    w_k: jax.Array144        ref_checkpoint: str | None = None,
66    w_v: jax.Array145    ) -> HFCheckpointConverter["GrugModelConfig"]:  # type: ignore[type-var]
67    w_o: jax.Array146        return HFCheckpointConverter(
68    cfg: GrugModelConfig = eqx.field(static=True)147            self.__class__,
148            reference_checkpoint=ref_checkpoint,
149            HfConfigClass=GrugMoeHfConfig,
150            tokenizer=ref_checkpoint,
151        )
69152
n70    @staticmethodn153    @classmethod
71    def init(cfg: GrugModelConfig, *, key: PRNGKeyArray) -> "CausalSelfAttention":154    def from_hf_config(cls, hf_config: HfConfig) -> "GrugModelConfig":
72        k_q, k_k, k_v, k_o = random.split(key, 4)155        rope = RotaryConfig(theta=float(_hf_config_attr(hf_config, ("rope_theta",), 10000.0)))
73        d_model, n_heads, n_kv_heads, head_dim = cfg.hidden_dim, cfg.num_heads, cfg.num_kv_heads, cfg.inferred_head_dim156        return cls(
74        return CausalSelfAttention(157            vocab_size=int(_hf_config_attr(hf_config, ("vocab_size",))),
75            w_q=reshard(_init_weight(k_q, (d_model, n_heads * head_dim), cfg.initializer_std), P("data", "model")),158            hidden_dim=int(_hf_config_attr(hf_config, ("hidden_dim", "hidden_size"), 2048)),
76            w_k=reshard(_init_weight(k_k, (d_model, n_kv_heads * head_dim), cfg.initializer_std), P("data", "model")),159            intermediate_dim=int(
77            w_v=reshard(_init_weight(k_v, (d_model, n_kv_heads * head_dim), cfg.initializer_std), P("data", "model")),160                _hf_config_attr(hf_config, ("intermediate_dim", "moe_intermediate_size", "intermediate_size"), 5632)
78            w_o=reshard(_init_weight(k_o, (n_heads * head_dim, d_model), cfg.initializer_std), P("model", "data")),
79            cfg=cfg,161            ),
162            shared_expert_intermediate_dim=int(
163                _hf_config_attr(
164                    hf_config,
165                    ("shared_expert_intermediate_dim", "shared_expert_intermediate_size"),
166                    5632,
167                )
168            ),
169            num_experts=int(_hf_config_attr(hf_config, ("num_experts", "num_local_experts"), 8)),
170            num_experts_per_token=int(_hf_config_attr(hf_config, ("num_experts_per_token", "num_experts_per_tok"), 2)),
171            num_layers=int(_hf_config_attr(hf_config, ("num_layers", "num_hidden_layers"), 24)),
172            num_heads=int(_hf_config_attr(hf_config, ("num_heads", "num_attention_heads"), 16)),
173            num_kv_heads=int(_hf_config_attr(hf_config, ("num_kv_heads", "num_key_value_heads"), 16)),
174            head_dim=_hf_config_attr(hf_config, ("head_dim", "attention_head_dim")),
175            max_seq_len=int(_hf_config_attr(hf_config, ("max_seq_len", "max_position_embeddings"), 4096)),
176            sliding_window=int(_hf_config_attr(hf_config, ("sliding_window",), 4096)),
177            layer_norm_eps=float(_hf_config_attr(hf_config, ("layer_norm_eps", "rms_norm_eps"), 1e-5)),
178            initializer_std=float(_hf_config_attr(hf_config, ("initializer_std", "initializer_range"), 0.02)),
179            qk_mult=float(_hf_config_attr(hf_config, ("qk_mult",), 1.0)),
180            rope=rope,
181        )
182 
183    def to_hf_config(self, vocab_size: int, config_overrides: dict[str, Any] | None = None) -> GrugMoeHfConfig:
184        # One name per field: core fields take the universal transformers spelling, MoE fields the
185        # most common public spelling, and grug-specific extras keep their bare names. from_hf_config
186        # stays tolerant of the older spellings so existing artifacts keep loading.
187        config = {
188            "architectures": [GRUG_MOE_ARCHITECTURE],
189            "vocab_size": vocab_size,
190            # core â€” universal transformers names
191            "hidden_size": self.hidden_dim,
192            "num_hidden_layers": self.num_layers,
193            "num_attention_heads": self.num_heads,
194            "num_key_value_heads": self.num_kv_heads,
195            "head_dim": self.inferred_head_dim,
196            "max_position_embeddings": self.max_seq_len,
197            "sliding_window": self.sliding_window,
198            "rms_norm_eps": self.layer_norm_eps,
199            "initializer_range": self.initializer_std,
200            "rope_theta": self.rope.theta,
201            "tie_word_embeddings": False,
202            # MoE â€” most common public spelling per field
203            "num_experts": self.num_experts,
204            "num_experts_per_tok": self.num_experts_per_token,
205            "moe_intermediate_size": self.intermediate_dim,
206            "shared_expert_intermediate_size": self.shared_expert_intermediate_dim,
207            # grug-specific (no public equivalent)
208            "qk_mult": self.qk_mult,
209            "grugmoe_attention_mode": "production",
210            GRUG_MOE_ARTIFACT_SCHEMA_VERSION_KEY: GRUG_MOE_ARTIFACT_SCHEMA_VERSION,
80        )211        }
81 212        if config_overrides is not None:
82    @trace_grads213            config.update(config_overrides)
83    @named_call214        return GrugMoeHfConfig(**config)
84    def __call__(self, x: Float[Array, "B S D"], mask: AttentionMask | jax.Array) -> Float[Array, "B S D"]:
85        head_dim = self.cfg.inferred_head_dim
86        seq_len = x.shape[1]
87 
88        q = rearrange(jnp.einsum("bsh,hd->bsd", x, self.w_q), "... (n d) -> ... n d", d=head_dim)
89        k = rearrange(jnp.einsum("bsh,hd->bsd", x, self.w_k), "... (m d) -> ... m d", d=head_dim)
90        v = rearrange(jnp.einsum("bsh,hd->bsd", x, self.w_v), "... (m d) -> ... m d", d=head_dim)
91        q, k = apply_rotary_embedding(q, k, seq_len=seq_len, head_dim=head_dim, rope=self.cfg.rope)
92        attn_out = attention(q, k, v, mask)
93        attn_out = rearrange(attn_out, "... n d -> ... (n d)")
94        return jnp.einsum("bsh,hd->bsd", attn_out, self.w_o, out_sharding=Pbatch)
95 
96 
97class MLP(eqx.Module):
98    mlp_up: jax.Array
99    mlp_down: jax.Array
100 
101    @staticmethod
102    def init(cfg: GrugModelConfig, *, key: PRNGKeyArray) -> "MLP":
103        k_up, k_down = random.split(key, 2)
104        d_model, d_ff = cfg.hidden_dim, cfg.intermediate_dim
105        return MLP(
106            mlp_up=reshard(_init_weight(k_up, (d_model, d_ff), cfg.initializer_std), P("data", "model")),
107            mlp_down=reshard(_init_weight(k_down, (d_ff, d_model), cfg.initializer_std), P("model", "data")),
108        )
109 
110    @trace_grads
111    @named_call
112    def __call__(self, x: Float[Array, "B S D"]) -> Float[Array, "B S D"]:
113        up = jnp.einsum("bsh,hm->bsm", x, self.mlp_up)
114        activated = jax.nn.relu(up)
115        return jnp.einsum("bsm,mh->bsh", activated, self.mlp_down, out_sharding=Pbatch)
116 
117 
118class RMSNorm(eqx.Module):
119    weight: jax.Array
120    eps: float = eqx.field(static=True)
121 
122    @staticmethod
123    def init(dim: int, eps: float) -> "RMSNorm":
124        return RMSNorm(weight=jnp.ones((dim,), dtype=jnp.float32), eps=eps)
125 
126    @named_call
127    def __call__(self, x: Float[Array, "... D"]) -> Float[Array, "... D"]:
128        weight = unshard(self.weight)
129        dtype = x.dtype
130        x = x.astype(jnp.float32)
131        variance = jnp.mean(jnp.square(x), axis=-1, keepdims=True)
132        normed = x * jax.lax.rsqrt(variance + self.eps)
133        return (normed * weight).astype(dtype)
134 
135 
136class Block(eqx.Module):
137    rms_attn: RMSNorm
138    attn: CausalSelfAttention
139    rms_mlp: RMSNorm
140    mlp: MLP
141 
142    @staticmethod
143    def init(cfg: GrugModelConfig, *, key: PRNGKeyArray) -> "Block":
144        attn_key, mlp_key = random.split(key, 2)
145        return Block(
146            rms_attn=RMSNorm.init(cfg.hidden_dim, cfg.layer_norm_eps),
147            attn=CausalSelfAttention.init(cfg, key=attn_key),
148            rms_mlp=RMSNorm.init(cfg.hidden_dim, cfg.layer_norm_eps),
149            mlp=MLP.init(cfg, key=mlp_key),
150        )
151 
152    @named_call
153    def __call__(self, x: Float[Array, "B S D"], mask: AttentionMask | jax.Array) -> Float[Array, "B S D"]:
154        x = trace_backward_activation(x, "resid_in")
155        x = x + self.attn(self.rms_attn(x), mask)
156        x = trace_backward_activation(x, "resid_post_attn")
157        x = x + self.mlp(self.rms_mlp(x))
158        return trace_backward_activation(x, "resid_out")
159215
160216
161class Transformer(eqx.Module):217class Transformer(eqx.Module):
162    token_embed: jax.Array218    token_embed: jax.Array
nn219    embed_norm: RMSNorm
220    embed_gated_norm: GatedNorm
163    output_proj: jax.Array221    output_proj: jax.Array
164    blocks: tuple[Block, ...]222    blocks: tuple[Block, ...]
165    final_norm: RMSNorm223    final_norm: RMSNorm
nn224    final_gated_norm: GatedNorm
166    config: GrugModelConfig = eqx.field(static=True)225    config: GrugModelConfig = eqx.field(static=True)
167226
168    @staticmethod227    @staticmethod
n169    def init(cfg: GrugModelConfig, *, key: PRNGKeyArray) -> "Transformer":n228    def init(
229        cfg_or_vocab: GrugModelConfig | Axis,
230        config: GrugModelConfig | None = None,
231        *,
232        key: PRNGKeyArray,
233    ) -> "Transformer":
234        if isinstance(cfg_or_vocab, Axis):
235            if config is None:
236                raise ValueError("config must be provided when initializing with a Vocab axis")
237            cfg = (
238                config
239                if cfg_or_vocab.size == config.vocab_size
240                else dataclasses.replace(config, vocab_size=cfg_or_vocab.size)
241            )
242        else:
243            if config is not None:
244                raise ValueError("config must not be provided when initializing directly from GrugModelConfig")
245            cfg = cfg_or_vocab
246 
170        embed_key, out_key, *block_keys = random.split(key, cfg.num_layers + 2)247        embed_key, out_key, embed_gn_key, final_gn_key, *block_keys = random.split(key, cfg.num_layers + 4)
171        token_embed = reshard(248        token_embed = reshard(
172            _init_weight(embed_key, (cfg.vocab_size, cfg.hidden_dim), cfg.initializer_std), Pembed_vocab249            _init_weight(embed_key, (cfg.vocab_size, cfg.hidden_dim), cfg.initializer_std), Pembed_vocab
173        )250        )
174        output_proj = reshard(_init_weight(out_key, (cfg.hidden_dim, cfg.vocab_size), cfg.initializer_std), Plm_head)251        output_proj = reshard(_init_weight(out_key, (cfg.hidden_dim, cfg.vocab_size), cfg.initializer_std), Plm_head)
n175        blocks = tuple(Block.init(cfg, key=layer_key) for layer_key in block_keys)n252        blocks = tuple(Block.init(cfg, key=block_keys[i]) for i in range(cfg.num_layers))
176        final_norm = RMSNorm.init(cfg.hidden_dim, cfg.layer_norm_eps)
177        return Transformer(253        return Transformer(
178            token_embed=token_embed,254            token_embed=token_embed,
nn255            embed_norm=RMSNorm.init(cfg.hidden_dim, cfg.layer_norm_eps),
256            embed_gated_norm=GatedNorm.init(cfg.hidden_dim, cfg.initializer_std, key=embed_gn_key),
179            output_proj=output_proj,257            output_proj=output_proj,
180            blocks=blocks,258            blocks=blocks,
n181            final_norm=final_norm,n259            final_norm=RMSNorm.init(cfg.hidden_dim, cfg.layer_norm_eps),
260            final_gated_norm=GatedNorm.init(cfg.hidden_dim, cfg.initializer_std, key=final_gn_key),
182            config=cfg,261            config=cfg,
183        )262        )
nn263 
264    @property
265    def Vocab(self) -> Axis:
266        return Axis("vocab", self.config.vocab_size)
184267
185    @named_call268    @named_call
186    def __call__(269    def __call__(
187        self,270        self,
188        token_ids: Int[Array, "B S"],271        token_ids: Int[Array, "B S"],
189        mask: AttentionMask | jax.Array | None = None,272        mask: AttentionMask | jax.Array | None = None,
n190    ) -> Float[Array, "B S D"]:n273    ) -> tuple[Float[Array, "B S D"], dict[str, jax.Array]]:
191        if mask is None:274        if mask is None:
192            mask = AttentionMask.causal()275            mask = AttentionMask.causal()
193276
n194        with jax.named_scope("token_embed"):n277        batch_spec = _batch_spec()
278        cfg = self.config
195            hidden = self.token_embed.at[token_ids].get(out_sharding=Pbatch)279        hidden = self.token_embed.at[token_ids].get(out_sharding=batch_spec)
196            hidden = log_backward_activation(hidden)280        hidden = self.embed_gated_norm(self.embed_norm(hidden))
281 
282        # Short layers: sliding window. Long layers (every 4th + last): full causal.
283        segment_ids = mask.segment_ids if isinstance(mask, AttentionMask) else None
284        short_mask = AttentionMask(is_causal=True, sliding_window=cfg.sliding_window, segment_ids=segment_ids)
285        long_mask = AttentionMask(is_causal=True, sliding_window=None, segment_ids=segment_ids)
286 
287        if cfg.remat_mode == "save_moe":
288            remat_policy = jax.checkpoint_policies.save_only_these_names(*MOE_REMAT_SAVE_NAMES)
289        else:
290            remat_policy = None
291 
292        num_blocks = len(self.blocks)
293        moe_router_stats: list[dict[str, jax.Array]] = []
197        for i, block in enumerate(self.blocks):294        for i, block in enumerate(self.blocks):
n198            with jax.named_scope(f"block_{i}"):n295            is_last = i == num_blocks - 1
199                block_fn = block if is_backward_flow_active() else eqx.filter_checkpoint(block)296            is_long = i % 4 == 3 or is_last
200                hidden = block_fn(hidden, mask)297            layer_mask = long_mask if is_long else short_mask
201        with jax.named_scope("final_norm"):298            hidden, router_stats = eqx.filter_checkpoint(block, policy=remat_policy)(
202            hidden = self.final_norm(hidden)299                hidden,
203            return log_backward_activation(hidden)300                layer_mask,
301                use_pko=is_long and not cfg.disable_pko,
302                disable_rope=is_long and cfg.disable_long_rope,
303            )
304            moe_router_stats.append(router_stats)
305 
306        router_metrics = {
307            "routing_entropy_per_layer": jnp.stack([s["routing_entropy"] for s in moe_router_stats], axis=0),
308            "routing_counts_per_layer": jnp.stack([s["routing_counts"] for s in moe_router_stats], axis=0),
309            "load_balancing_loss_per_layer": jnp.stack([s["load_balancing_loss"] for s in moe_router_stats], axis=0),
310            "router_z_loss_per_layer": jnp.stack([s["router_z_loss"] for s in moe_router_stats], axis=0),
311            "qb_beta_per_layer": jnp.stack([s["qb_beta"] for s in moe_router_stats], axis=0),
312            "capacity_overflow_per_layer": jnp.stack([s["capacity_overflow"] for s in moe_router_stats], axis=0),
313        }
314        hidden = self.final_gated_norm(self.final_norm(hidden))
315        return hidden, router_metrics
204316
205    @named_call317    @named_call
206    def logits(318    def logits(
208        token_ids: Int[Array, "B S"],320        token_ids: Int[Array, "B S"],
209        mask: AttentionMask | jax.Array | None = None,321        mask: AttentionMask | jax.Array | None = None,
210    ) -> Float[Array, "B S V"]:322    ) -> Float[Array, "B S V"]:
nn323        batch_spec = _batch_spec()
211        hidden = self(token_ids, mask=mask)324        hidden, _ = self(token_ids, mask=mask)
212        return jnp.einsum("bsh,hd->bsd", hidden, self.output_proj, out_sharding=Plogits)325        return jnp.einsum("bsh,hd->bsd", hidden, self.output_proj, out_sharding=batch_spec)
326 
327    def to_state_dict(self, prefix: str | None = None) -> dict[str, jax.Array]:
328        return grugmoe_inference_state_dict(self, prefix=prefix)
213329
214    def next_token_loss(330    def next_token_loss(
215        self,331        self,
220        reduction: str = "mean",336        reduction: str = "mean",
221        logsumexp_weight: float | None = None,337        logsumexp_weight: float | None = None,
222        loss_dtype: jnp.dtype = jnp.float32,338        loss_dtype: jnp.dtype = jnp.float32,
n223        loss_implementation: str | tuple[str, ...] | None = None,n339        return_router_metrics: bool = False,
224    ) -> jax.Array:340    ) -> jax.Array | tuple[jax.Array, dict[str, jax.Array | SummaryStats]]:
225        """Compute next-token cross-entropy loss for a batch."""
226        hidden = self(token_ids, mask=mask)341        hidden, router_metrics = self(token_ids, mask=mask)
227        labels = jnp.pad(token_ids[:, 1:], ((0, 0), (0, 1))).astype(jnp.int32)342        labels = jnp.pad(token_ids[:, 1:], ((0, 0), (0, 1))).astype(jnp.int32)
228        loss_weight = loss_weight.astype(loss_dtype)343        loss_weight = loss_weight.astype(loss_dtype)
229344
n230        return fused_linear_softmax_cross_entropy_loss(n345        cross_entropy_loss = fused_linear_softmax_cross_entropy_loss(
231            hidden,346            hidden,
232            self.output_proj,347            self.output_proj,
233            labels,348            labels,
235            reduction=reduction,350            reduction=reduction,
236            logsumexp_weight=logsumexp_weight,351            logsumexp_weight=logsumexp_weight,
237            dtype=loss_dtype,352            dtype=loss_dtype,
n238            implementation=loss_implementation,n
239        )
240 
241 
242def _init_weight(key: PRNGKeyArray, shape: tuple[int, ...], std: float) -> Float[Array, "..."]:
243    return std * random.truncated_normal(key, -3, 3, shape)
244 
245 
246def debug_mesh_and_token_pspec(num_devices: int, model_axis_size: int = 1) -> tuple[jax.sharding.AbstractMesh, P]:
247    """Return a small abstract mesh and token sharding for lowering contract tests."""
248    if num_devices <= 0:
249        raise ValueError(f"num_devices must be positive, got {num_devices}")
250    if model_axis_size <= 0:
251        raise ValueError(f"model_axis_size must be positive, got {model_axis_size}")
252    if num_devices % model_axis_size != 0:
253        raise ValueError(f"num_devices ({num_devices}) must be divisible by model_axis_size ({model_axis_size})")
254    data_axis_size = num_devices // model_axis_size
255    mesh = jax.sharding.AbstractMesh(
256        axis_sizes=(1, data_axis_size, model_axis_size),
257        axis_names=("replica_dcn", "data", "model"),
258        axis_types=(
259            jax.sharding.AxisType.Explicit,
260            jax.sharding.AxisType.Explicit,
261            jax.sharding.AxisType.Explicit,
262        ),353        )
263    )354        # No load-balancing loss; router z-loss only.
264    return mesh, P(("replica_dcn", "data"), None)355        num_moe_layers = router_metrics["router_z_loss_per_layer"].shape[0]
356        router_z_loss = jnp.sum(router_metrics["router_z_loss_per_layer"]) / num_moe_layers
357        aux_loss = self.config.router_z_loss_coef * router_z_loss
358        loss = cross_entropy_loss + aux_loss if reduction != "none" else cross_entropy_loss
359        if return_router_metrics:
360            summarized_metrics = _summarize_router_metrics(router_metrics)
361            summarized_metrics["train/cross_entropy_loss"] = cross_entropy_loss
362            summarized_metrics["train/router/aux_loss_weighted"] = aux_loss
363            return loss, summarized_metrics
364        return loss
265365
266366
267__all__ = [367__all__ = [
n268    "MLP",n368    "GRUG_MOE_ARCHITECTURE",
369    "GRUG_MOE_ARTIFACT_SCHEMA_VERSION",
370    "GRUG_MOE_ARTIFACT_SCHEMA_VERSION_KEY",
371    "GRUG_MOE_MODEL_TYPE",
269    "Block",372    "Block",
270    "CausalSelfAttention",373    "CausalSelfAttention",
nn374    "DenseMLP",
375    "GatedNorm",
271    "GrugModelConfig",376    "GrugModelConfig",
nn377    "GrugMoeHfConfig",
378    "MoEMLP",
379    "MoeActivation",
272    "RMSNorm",380    "RMSNorm",
273    "Transformer",381    "Transformer",
274    "debug_mesh_and_token_pspec",382    "debug_mesh_and_token_pspec",
tt383    "grugmoe_inference_state_dict",
275]384]

Back to top

train.py

changed

experiments/grug/base/train.py
experiments/grug/moe_pipeline/train.py
f1# Copyright The Marin Authorsf1# Copyright The Marin Authors
2# SPDX-License-Identifier: Apache-2.02# SPDX-License-Identifier: Apache-2.0
33
n4import dataclassesn4"""Pipeline-parallel training for the Grug MoE pipeline variant."""
5import functools5 
6import logging6# GRUG NOVERIFY: JaxPP's MPMD lowering requires a multi-process accelerator mesh.
7 
8import json
9import os
10import statistics
7import time11import time
n8from dataclasses import dataclass, fieldn12from dataclasses import dataclass
13from enum import StrEnum
14from types import SimpleNamespace
15from typing import cast
916
n10import equinox as eqxn
11import jax17import jax
12import jax.numpy as jnp18import jax.numpy as jnp
13import jmp19import jmp
n14import levanter.callbacks as callbacksn20import numpy as np
15import levanter.tracker
16import optax21import optax
17from fray.cluster import ResourceConfig22from fray.cluster import ResourceConfig
n18from haliax import Axisn23from fray.device_flops import device_flops
19from haliax.partitioning import set_mesh24from iris.runtime.jax_init import initialize_jax
20from jax.sharding import Mesh, NamedSharding25from jax.sharding import NamedSharding
21from jax.sharding import PartitionSpec as P26from jax.sharding import PartitionSpec as P
n22from jax.tree_util import register_dataclassn27from levanter.callbacks.profiler import ProfileOptionsConfig, ProfilerConfig
23from jaxtyping import PRNGKeyArray28from levanter.data.text.examples import GrugLmExample
24from levanter.analysis.backward_flow import (29from levanter.pipeline import reshape_batch_into_microbatches
25    BackwardFlowConfig,30from levanter.utils.flop_utils import lm_flops_per_token
26    BackwardFlowGraph,31 
27    backward_flow_graph_from_jaxpr,32from experiments.grug.dispatch import dispatch_grug_training_run
28    backward_flow_node_stats,33from experiments.grug.moe_pipeline.model import BATCH_AXES, GrugModelConfig, Transformer
29    capture_backward_flow,34from experiments.grug.moe_pipeline.pipeline import (
30    collapse_backward_flow_graph,35    TRAIN_LOSS_KEY,
31    infer_backward_flow_render_hints,36    AutomaticPipelineSchedule,
32    render_backward_flow_html,37    GrugMoePipelineConfig,
38    automatic_stage_to_mpmd_indices,
39    initialize_mpmd_automatic_pipeline_state,
40    make_automatic_pipeline_step,
41    make_pipeline_mesh,
42    prepare_automatic_mpmd_step,
33)43)
n34from levanter.callbacks.state_adapter import StateCallbackRunnern
35from levanter.callbacks.watch import WatchConfig, compute_watch_stats
36from levanter.data.dataset import AsyncDataset
37from levanter.data.loader import DataLoader
38from levanter.data.mixture import MixtureDataset, rescale_mixture_schedule_for_batch_schedule
39from levanter.data.text.datasets import LmDataConfig
40from levanter.data.text.examples import GrugLmExample, grug_lm_example_from_named
41from levanter.eval import TaggedEvaluator, cb_tagged_evaluate
42from levanter.grug.sharding import compact_grug_mesh
43from levanter.models.lm_model import LmExample
44from levanter.optim.config import AdamConfig, OptimizerConfig
45from levanter.schedule import BatchSchedule
46from levanter.trainer import TrainerConfig
47from levanter.training_control import TrainingDashboard
48from levanter.utils.flop_utils import lm_flops_per_token
49from levanter.utils.jax_utils import parameter_count
50from levanter.utils.logging import LoadingTimeTrackerIterator
5144
n52from experiments.grug.base.model import GrugModelConfig, Transformern
53from experiments.grug.checkpointing import init_weights_only_from_checkpoint, restore_grug_state_from_checkpoint
54from experiments.grug.dispatch import dispatch_grug_training_run
55from experiments.grug.sharding_dump import dump_grug_state_sharding_run_artifact
5645
n57logger = logging.getLogger(__name__)n46class PipelineSchedule(StrEnum):
58_BACKWARD_FLOW_METRICS_KEY = "_backward_flow"47    ZERO_BUBBLE = "automatic_zero_bubble"
59_BACKWARD_FLOW_DEFAULT_INTERVAL = 5048    DUALPIPE_V = "automatic_dualpipe_v"
49 
50    @property
51    def automatic_schedule(self) -> AutomaticPipelineSchedule:
52        if self == self.DUALPIPE_V:
53            return AutomaticPipelineSchedule.DUALPIPE_V
54        if self == self.ZERO_BUBBLE:
55            return AutomaticPipelineSchedule.ZERO_BUBBLE
56        raise ValueError(f"unknown pipeline schedule: {self.value}")
6057
6158
62@dataclass(frozen=True)59@dataclass(frozen=True)
n63class GrugTrainerConfig:n60class GrugPipelineTrainConfig:
64    """Runtime knobs for grug training."""61    """Model, topology, and loop settings for pipeline-parallel training."""
6562
n66    trainer: TrainerConfig = field(default_factory=lambda: TrainerConfig(use_explicit_mesh_axes=True))n63    stages: int
67    train_batch_pspec: P = field(default_factory=lambda: P(("replica_dcn", "data")))64    physical_stages: int
68    data_seed: int | None = None65    microbatches: int
69    log_every: int = 1
70    ema_beta: float | None = None  # EMA coefficient for eval/checkpoint model; None disables EMA.
71    z_loss_weight: float = 0.0  # Weight on logsumexp (z-loss) stabilization term.
72    backward_flow: BackwardFlowConfig = field(
73        default_factory=lambda: BackwardFlowConfig(interval=_BACKWARD_FLOW_DEFAULT_INTERVAL)
74    )
75    loss_implementation: str | tuple[str, ...] | None = None
76    sharding_dump_path: str | None = None
77 
78 
79@dataclass(frozen=True)
80class GrugEvalConfig:
81    """Perplexity eval settings for grug training."""
82 
83    eval_batch_size: int = 51266    batch_size: int
84    eval_batch_pspec: P = field(default_factory=lambda: P(("replica_dcn", "data")))67    seq_len: int
85    steps_per_eval: int | None = 100068    hidden_dim: int
86    max_eval_batches: int | None = None69    intermediate_dim: int
87    prefix: str = "eval"70    shared_expert_intermediate_dim: int
88    eval_current: bool = True71    num_layers: int
89    eval_ema: bool = True72    num_experts: int
90    compute_bpb: bool = True73    top_k: int
74    expert_axis_size: int
75    vocab_size: int
76    num_heads: int
77    num_kv_heads: int
78    sliding_window: int
79    qk_mult: float
80    steps: int
81    warmup_steps: int
82    profile_start_step: int
83    profile_steps: int
84    profile_run_id: str | None
85    layer_counts: tuple[int, ...] | None
86    memory_threshold: int | None
87    mp_policy_string: str
88    remat_mode: str
89    attention_implementation: str
90    moe_implementation: str
91    schedule: PipelineSchedule
92 
93    def __post_init__(self) -> None:
94        if not isinstance(self.schedule, PipelineSchedule):
95            raise ValueError(f"unknown pipeline schedule: {self.schedule!r}")
96        if self.schedule == PipelineSchedule.ZERO_BUBBLE and self.stages != self.physical_stages:
97            raise ValueError(
98                "automatic_zero_bubble requires one logical stage per physical stage; "
99                f"got {self.stages} logical and {self.physical_stages} physical stages"
100            )
101        if self.schedule == PipelineSchedule.DUALPIPE_V:
102            if self.stages != 2 * self.physical_stages:
103                raise ValueError(
104                    "automatic_dualpipe_v requires two logical stages per physical stage; "
105                    f"got {self.stages} logical and {self.physical_stages} physical stages"
106                )
107            if self.microbatches < self.stages:
108                raise ValueError(
109                    f"automatic_dualpipe_v requires at least {self.stages} microbatches, got {self.microbatches}"
110                )
91111
92112
93@dataclass(frozen=True)113@dataclass(frozen=True)
94class GrugRunConfig:114class GrugRunConfig:
n95    """Top-level config for grug training."""n115    """Resources and identity for one pipeline-parallel training run."""
96116
n97    model: GrugModelConfign117    run_id: str
98    data: LmDataConfig118    train: GrugPipelineTrainConfig
99    resources: ResourceConfig119    resources: ResourceConfig
n100    output_path: str = ""n
101    optimizer: OptimizerConfig = field(default_factory=AdamConfig)
102    trainer: GrugTrainerConfig = field(default_factory=GrugTrainerConfig)
103    eval: GrugEvalConfig | None = field(default_factory=GrugEvalConfig)
104120
105121
n106def build_train_dataset(n122def _log(event: str, **values) -> None:
107    data_config: LmDataConfig,123    print(f"{event} {json.dumps(values, sort_keys=True, default=str)}", flush=True)
124 
125 
126def _validate_local_mesh(
108    *,127    *,
n109    max_seq_len: int,n128    local_device_count: int,
110    batch_schedule: BatchSchedule,129    devices_per_stage: int,
111    key: PRNGKeyArray,130    expert_axis_size: int,
112) -> MixtureDataset[GrugLmExample]:131    batch_size: int,
113    pos = Axis("position", max_seq_len)132    microbatches: int,
114    mix_key, shuffle_key = jax.random.split(key)133) -> None:
115    weights = data_config.train_weights134    if local_device_count % expert_axis_size != 0:
116    if isinstance(weights, list):135        raise ValueError(f"local device count {local_device_count} must be divisible by expert axis {expert_axis_size}")
117        weights = rescale_mixture_schedule_for_batch_schedule(weights, batch_schedule)136    if batch_size % microbatches != 0:
118 137        raise ValueError(f"batch size {batch_size} must be divisible by {microbatches} microbatches")
119    initial_batch_size = batch_schedule.batch_size_at_step(0)138    microbatch_size = batch_size // microbatches
120    datasets = data_config.train_sets(pos, key=shuffle_key, initial_batch_size=initial_batch_size)139    if microbatch_size % devices_per_stage != 0:
121    return MixtureDataset(140        raise ValueError(
122        datasets=datasets,141            f"microbatch size {microbatch_size} must be divisible by the {devices_per_stage} devices in each stage"
123        weights=weights,
124        stop_strategy=data_config.stop_strategy,
125        key=mix_key,
126        block_size=data_config.mixture_block_size,
127    )
128 
129 
130def build_train_loader(
131    dataset: AsyncDataset[GrugLmExample],
132    *,
133    batch_schedule: BatchSchedule,
134    mesh: Mesh,
135    batch_pspec: P = P(("replica_dcn", "data")),
136) -> DataLoader[GrugLmExample]:
137    # DataLoader uses this batch axis mapping to shard batches across the distributed mesh.
138    axis_resource = batch_pspec[0]
139    return DataLoader(
140        dataset,
141        batch_schedule.schedule,
142        mesh=mesh,
143        axis_resources={"__BATCH__": axis_resource},
144        batch_axis_name="__BATCH__",
145        allow_nondivisible_batch_size=False,
146    )
147 
148 
149def build_tagged_evaluator(
150    *,
151    data_config: LmDataConfig,
152    max_seq_len: int,
153    mesh: Mesh,
154    eval_cfg: GrugEvalConfig,
155) -> TaggedEvaluator[LmExample | GrugLmExample, Transformer] | None:
156    pos = Axis("position", max_seq_len)
157    tagged_eval_sets = data_config.tagged_eval_sets(pos)
158    if len(tagged_eval_sets) == 0:
159        logger.warning("No evaluation datasets provided.")
160        return None
161 
162    max_examples_per_dataset = None
163    if eval_cfg.max_eval_batches is not None:
164        max_examples_per_dataset = eval_cfg.max_eval_batches * eval_cfg.eval_batch_size
165 
166    tokenizer = data_config.the_tokenizer if eval_cfg.compute_bpb else None
167    batch_axis_resource = eval_cfg.eval_batch_pspec[0]
168    eval_axis_mapping = {"batch": batch_axis_resource}
169    eval_batch = Axis("batch", eval_cfg.eval_batch_size)
170    eval_array_sharding = NamedSharding(mesh, P(batch_axis_resource, None))
171 
172    def eval_loss_fn(model: Transformer, batch: LmExample | GrugLmExample) -> tuple[jax.Array, jax.Array, jax.Array]:
173        if isinstance(batch, LmExample):
174            batch = grug_lm_example_from_named(batch)
175        per_pos_loss = model.next_token_loss(
176            batch.tokens,
177            batch.loss_weight,
178            mask=batch.attn_mask,
179            reduction="none",
180            logsumexp_weight=None,
181        )142        )
n182        per_pos_loss = jax.sharding.reshard(per_pos_loss, eval_array_sharding)n
183        per_pos_weight = jax.sharding.reshard(batch.loss_weight, eval_array_sharding)
184        per_pos_token_id = jnp.pad(batch.tokens[:, 1:], ((0, 0), (0, 1)))
185        return per_pos_loss, per_pos_weight, per_pos_token_id
186143
n187    return TaggedEvaluator(n
188        EvalBatch=eval_batch,
189        tagged_eval_sets=tagged_eval_sets,
190        loss_fn=eval_loss_fn,
191        tokenizer=tokenizer,
192        device_mesh=mesh,
193        axis_mapping=eval_axis_mapping,
194        max_examples_per_dataset=max_examples_per_dataset,
195    )
196144
nn145def _run_grug_local(config: GrugPipelineTrainConfig) -> None:
146    os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false")
147    initialize_jax()
197148
n198def _compute_flops(n149    from jaxpp.array import MpmdArray  # noqa: PLC0415  # pyrefly: ignore[missing-import]
199    *,150 
151    mp_policy = jmp.get_policy(config.mp_policy_string)
152    pipeline_config = GrugMoePipelineConfig(
153        stages=config.stages,
154        microbatches=config.microbatches,
155        physical_stages=None if config.physical_stages == config.stages else config.physical_stages,
156    )
157 
158    if jax.process_count() % config.physical_stages != 0:
159        raise ValueError(
160            f"process count {jax.process_count()} must be divisible by {config.physical_stages} physical stages"
161        )
162    replica_axis_size = jax.process_count() // config.physical_stages
163    _validate_local_mesh(
164        local_device_count=jax.local_device_count(),
165        devices_per_stage=jax.device_count() // config.physical_stages,
166        expert_axis_size=config.expert_axis_size,
167        batch_size=config.batch_size,
168        microbatches=config.microbatches,
169    )
170 
200    model_config: GrugModelConfig,171    model_config = GrugModelConfig(
201) -> tuple[float, dict[str, float]]:172        vocab_size=config.vocab_size,
173        hidden_dim=config.hidden_dim,
174        intermediate_dim=config.intermediate_dim,
175        shared_expert_intermediate_dim=config.shared_expert_intermediate_dim,
176        num_layers=config.num_layers,
177        num_experts=config.num_experts,
178        num_experts_per_token=config.top_k,
179        num_heads=config.num_heads,
180        num_kv_heads=config.num_kv_heads,
181        max_seq_len=config.seq_len,
182        sliding_window=config.sliding_window,
183        qk_mult=config.qk_mult,
184        router_z_loss_coef=0.0,
185        attention_implementation=cast(str, config.attention_implementation),
186        moe_implementation=cast(str, config.moe_implementation),
187        remat_mode=cast(str, config.remat_mode),
188    )
189    mesh, mpmd_mesh = make_pipeline_mesh(
190        pipeline_config,
191        expert_axis_size=config.expert_axis_size,
192        replica_axis_size=replica_axis_size,
193    )
194    optimizer = optax.adamw(learning_rate=1e-4, b1=0.9, b2=0.95, weight_decay=0.1)
195    peak_flops_per_device = device_flops("h100")
196    if peak_flops_per_device is None:
197        raise ValueError("Fray does not define H100 BF16 peak FLOP/s")
198 
199    _log(
200        "PIPELINE_CONFIG",
201        process_index=jax.process_index(),
202        process_count=jax.process_count(),
203        global_devices=jax.device_count(),
204        local_devices=jax.local_device_count(),
205        mesh_shape=dict(mesh.shape),
206        stages=config.stages,
207        physical_stages=config.physical_stages,
208        microbatches=config.microbatches,
209        batch_size=config.batch_size,
210        seq_len=config.seq_len,
211        hidden_dim=config.hidden_dim,
212        intermediate_dim=model_config.intermediate_dim,
213        shared_expert_intermediate_dim=model_config.shared_expert_intermediate_dim,
214        num_layers=config.num_layers,
215        layers_per_stage=config.layer_counts,
216        num_experts=config.num_experts,
217        top_k=config.top_k,
218        vocab_size=model_config.vocab_size,
219        num_heads=model_config.num_heads,
220        num_kv_heads=model_config.num_kv_heads,
221        sliding_window=model_config.sliding_window,
222        qk_mult=model_config.qk_mult,
223        steps=config.steps,
224        profile_start_step=config.profile_start_step,
225        profile_steps=config.profile_steps,
226        mp_policy=config.mp_policy_string,
227        remat_mode=config.remat_mode,
228        schedule=config.schedule,
229        jax_version=jax.__version__,
230    )
231 
232    init_started = time.monotonic()
233    with jax.set_mesh(mesh):
234        model = mp_policy.cast_to_param(Transformer.init(model_config, key=jax.random.PRNGKey(0)))
235        batch_sharding = NamedSharding(mesh, P(BATCH_AXES, None))
236        token_row = np.arange(config.seq_len, dtype=np.int32) % model_config.vocab_size
237        host_tokens = np.broadcast_to(token_row, (config.batch_size, config.seq_len)).copy()
238        host_loss_weight = np.ones((config.batch_size, config.seq_len), dtype=np.float32)
239        host_loss_weight[:, -1] = 0
240        batch = GrugLmExample(
241            tokens=jax.device_put(host_tokens, batch_sharding),
242            loss_weight=jax.device_put(host_loss_weight, batch_sharding),
243        )
244        loss_denominator = jnp.sum(batch.loss_weight.astype(jnp.float32))
245        batches = reshape_batch_into_microbatches(batch, config.microbatches)
246        if config.schedule == PipelineSchedule.DUALPIPE_V:
247            stage_to_mpmd_index = automatic_stage_to_mpmd_indices(pipeline_config, config.schedule.automatic_schedule)
248        else:
249            stage_to_mpmd_index = None
250        state, static_stages = initialize_mpmd_automatic_pipeline_state(
251            model,
252            optimizer,
253            mpmd_mesh,
254            num_stages=config.stages,
255            layer_counts=config.layer_counts,
256            stage_to_mpmd_index=stage_to_mpmd_index,
257            memory_threshold=config.memory_threshold,
258        )
259    jax.block_until_ready((state, batches, loss_denominator))
260    _log(
261        "PIPELINE_INIT",
262        process_index=jax.process_index(),
263        elapsed_seconds=time.monotonic() - init_started,
264    )
265 
266    build_started = time.monotonic()
267    step = make_automatic_pipeline_step(
268        optimizer,
269        mp_policy,
270        static_stages,
271        state,
272        batches,
273        config=pipeline_config,
274        mpmd_mesh=mpmd_mesh,
275        schedule_name=config.schedule.automatic_schedule,
276    )
277    _log(
278        "PIPELINE_BUILD",
279        process_index=jax.process_index(),
280        elapsed_seconds=time.monotonic() - build_started,
281    )
282    lower_started = time.monotonic()
283    prepared = prepare_automatic_mpmd_step(
284        step,
285        state,
286        batches,
287        loss_denominator,
288        mpmd_mesh,
289        memory_threshold=config.memory_threshold,
290    )
291    step = prepared.step
292    state = prepared.state
293    batches = prepared.batches
294    loss_denominator = prepared.loss_denominator
295    _log(
296        "PIPELINE_LOWER",
297        process_index=jax.process_index(),
298        elapsed_seconds=time.monotonic() - lower_started,
299    )
300 
301    profiler_callback = None
302    if config.profile_steps > 0:
303        profiler_callback = ProfilerConfig(
304            enabled=True,
305            start_step=config.profile_start_step,
306            num_steps=config.profile_steps,
307            perfetto_link=False,
308            profile_options=ProfileOptionsConfig(
309                host_tracer_level=1,
310                python_tracer_level=0,
311                enable_hlo_proto=True,
312            ),
313        ).build(
314            "/tmp/grug-moe-pipeline-profiler",
315            run_id=config.profile_run_id,
316        )
317        profiler_callback(SimpleNamespace(step=-1))
318 
319    step_times = []
320    loss = None
321    for step_index in range(config.steps):
322        started = time.monotonic()
323        state, metrics = step(state, batches, loss_denominator)
324        jax.block_until_ready((state, metrics))
325        elapsed = time.monotonic() - started
326        step_times.append(elapsed)
327        metric_loss = metrics[TRAIN_LOSS_KEY]
328        if profiler_callback is not None:
329            profiler_callback(SimpleNamespace(step=step_index))
330        if isinstance(metric_loss, MpmdArray):
331            if not metric_loss.is_partially_addressable:
332                continue
333            local_loss = metric_loss.to_mpmd_local_array
334            if not isinstance(local_loss, jax.Array):
335                raise TypeError(f"expected one local loss array, got {type(local_loss).__name__}")
336        else:
337            local_loss = metric_loss
338        loss = float(local_loss)
339        _log(
340            "PIPELINE_STEP",
341            step=step_index,
342            elapsed_seconds=elapsed,
343            tokens_per_second=config.batch_size * config.seq_len / elapsed,
344            loss=loss,
345        )
346 
347    measured = step_times[config.warmup_steps :]
348    if not measured:
349        measured = step_times
350    mean_step_seconds = sum(measured) / len(measured)
351    median_step_seconds = statistics.median(measured)
352    tokens_per_second = config.batch_size * config.seq_len / mean_step_seconds
353    median_tokens_per_second = config.batch_size * config.seq_len / median_step_seconds
202    flops_per_token = lm_flops_per_token(354    forward_flops_per_token = lm_flops_per_token(
203        hidden_dim=model_config.hidden_dim,355        hidden_dim=model_config.hidden_dim,
204        intermediate_dim=model_config.intermediate_dim,356        intermediate_dim=model_config.intermediate_dim,
nn357        shared_intermediate_dim=model_config.shared_expert_intermediate_dim,
205        num_layers=model_config.num_layers,358        num_layers=model_config.num_layers,
206        num_kv_heads=model_config.num_kv_heads,359        num_kv_heads=model_config.num_kv_heads,
207        num_heads=model_config.num_heads,360        num_heads=model_config.num_heads,
208        seq_len=model_config.max_seq_len,361        seq_len=model_config.max_seq_len,
209        vocab_size=model_config.vocab_size,362        vocab_size=model_config.vocab_size,
n210        glu=False,n363        glu=True,
211    )364        num_experts=model_config.num_experts,
212    flops_per_example = 3 * flops_per_token * model_config.max_seq_len365        num_shared_experts=1 if model_config.shared_expert_intermediate_dim > 0 else 0,
213 366        num_experts_per_tok=model_config.num_experts_per_token,
214    flops_summary: dict[str, float] = {
215        "throughput/flops_per_token_analytic": flops_per_token,
216        "throughput/flops_per_example_analytic": flops_per_example,
217    }
218 
219    return flops_per_example, flops_summary
220 
221 
222def _make_mixture_stage_callback(train_dataset: MixtureDataset, batch_schedule: BatchSchedule):
223    last_mixture_stage = -1
224 
225    def log_mixture_stage(step_info):
226        nonlocal last_mixture_stage
227        seq_index = batch_schedule.global_data_offset_by_step(step_info.step)
228        block_id = seq_index // train_dataset.block_size
229        stage = train_dataset._get_stage_for_block(block_id)
230        if stage == last_mixture_stage:
231            return
232 
233        weights = train_dataset.weight_stages[stage][1]
234        mixture_log = {f"mixture/weight/{name}": weight for name, weight in weights.items()}
235        mixture_log["mixture/stage"] = stage
236        levanter.tracker.log(mixture_log, step=step_info.step)
237        last_mixture_stage = stage
238 
239    return log_mixture_stage
240 
241 
242def _build_backward_flow_graph(
243    *,
244    params: Transformer,
245    batch: GrugLmExample,
246    mp: jmp.Policy,
247    z_loss_weight: float,
248) -> BackwardFlowGraph:
249    z_loss = z_loss_weight if z_loss_weight > 0 else None
250 
251    def loss_fn(model: Transformer) -> jax.Array:
252        compute_params = mp.cast_to_compute(model)
253        return compute_params.next_token_loss(
254            batch.tokens,
255            batch.loss_weight,
256            mask=batch.attn_mask,
257            reduction="mean",
258            logsumexp_weight=z_loss,
259        )
260 
261    with capture_backward_flow(BackwardFlowConfig(interval=1)):
262        closed_jaxpr, _, _ = eqx.filter_make_jaxpr(jax.grad(loss_fn))(params)
263    return backward_flow_graph_from_jaxpr(closed_jaxpr)
264 
265 
266def _write_backward_flow_artifact(
267    *,
268    graph: BackwardFlowGraph,
269    backward_flow_metrics: dict[str, jax.Array],
270    log_dir,
271    run_id: str,
272    step: int,
273) -> None:
274    node_stats = backward_flow_node_stats(backward_flow_metrics)
275    collapsed_graph = collapse_backward_flow_graph(graph, node_stats)
276    if not collapsed_graph.nodes:
277        return
278 
279    render_hints = infer_backward_flow_render_hints(collapsed_graph)
280    html = render_backward_flow_html(
281        collapsed_graph,
282        node_stats,
283        node_lanes=render_hints.node_lanes,
284        display_edges=render_hints.display_edges,
285        plates=render_hints.plates,
286        title=f"Backward Flow Step {step}",
287    )
288    artifact_dir = log_dir / run_id / "artifacts" / "backward_flow"
289    artifact_dir.mkdir(parents=True, exist_ok=True)
290    artifact_path = artifact_dir / f"step_{step:07d}.html"
291    artifact_path.write_text(html)
292    tracker = levanter.tracker.current_tracker()
293    tracker.log_html("backward_flow/dag", artifact_path, step=step, commit=False)
294    tracker.log_artifact(
295        artifact_path,
296        name=f"backward_flow_step_{step:07d}",
297        type="backward_flow",
298    )
299 
300 
301@register_dataclass
302@dataclass(frozen=True)
303class GrugTrainState:
304    step: jax.Array
305    params: Transformer
306    opt_state: optax.OptState
307    ema_params: Transformer | None
308 
309 
310def initial_state(
311    model_config: GrugModelConfig,
312    *,
313    optimizer: optax.GradientTransformation,
314    mp: jmp.Policy,
315    key: PRNGKeyArray,
316    ema_beta: float | None,
317) -> GrugTrainState:
318    params = mp.cast_to_param(Transformer.init(model_config, key=key))
319    return GrugTrainState(
320        step=jnp.array(0, dtype=jnp.int32),
321        params=params,
322        opt_state=optimizer.init(params),
323        ema_params=params if ema_beta is not None else None,
324    )
325 
326 
327def _make_train_step(
328    optimizer: optax.GradientTransformation,
329    mp: jmp.Policy,
330    *,
331    z_loss_weight: float,
332    loss_implementation: str | tuple[str, ...] | None = None,
333    ema_beta: float | None,
334    watch_config: WatchConfig | None = None,
335    backward_flow_config: BackwardFlowConfig | None = None,
336):
337    one = jnp.array(1, dtype=jnp.int32)
338    z_loss = z_loss_weight if z_loss_weight > 0 else None
339    if watch_config is not None:
340        if isinstance(watch_config.watch_targets, str):
341            watch_targets = tuple(t.strip() for t in watch_config.watch_targets.split(","))
342        else:
343            watch_targets = tuple(watch_config.watch_targets)
344    else:
345        watch_targets = ()
346 
347    @functools.partial(jax.jit, donate_argnums=(0,), static_argnames=("compute_backward_flow", "compute_watch"))
348    def train_step(
349        state: GrugTrainState,
350        batch,
351        *,
352        compute_watch: bool = False,
353        compute_backward_flow: bool = False,
354    ):367    )
355        def loss_fn(params):368    analytic_mfu = 3 * forward_flops_per_token * tokens_per_second / (jax.device_count() * peak_flops_per_device)
356            compute_params = mp.cast_to_compute(params)369    median_analytic_mfu = (
357            return compute_params.next_token_loss(370        3 * forward_flops_per_token * median_tokens_per_second / (jax.device_count() * peak_flops_per_device)
358                batch.tokens,371    )
359                batch.loss_weight,372    if loss is not None:
360                mask=batch.attn_mask,373        _log(
361                reduction="mean",374            "PIPELINE_SUMMARY",
362                logsumexp_weight=z_loss,375            mean_step_seconds=mean_step_seconds,
363                loss_implementation=loss_implementation,376            median_step_seconds=median_step_seconds,
377            tokens_per_second=tokens_per_second,
378            median_tokens_per_second=median_tokens_per_second,
379            analytic_mfu=analytic_mfu,
380            median_analytic_mfu=median_analytic_mfu,
381            measured_step_seconds=measured,
382            loss=loss,
383            measured_steps=len(measured),
384        )
385 
386    for device in jax.local_devices():
387        stats = device.memory_stats()
388        if stats is not None:
389            _log(
390                "PIPELINE_MEMORY",
391                process_index=jax.process_index(),
392                device_id=device.id,
393                peak_bytes_in_use=stats.get("peak_bytes_in_use"),
394                bytes_limit=stats.get("bytes_limit"),
364            )395            )
365396
n366        backward_flow_stats = Nonen
367        if backward_flow_config is not None and compute_backward_flow:
368            gradient_scale = jnp.sum(jnp.asarray(batch.loss_weight, dtype=jnp.float32))
369            with capture_backward_flow(backward_flow_config, gradient_scale=gradient_scale):
370                with levanter.tracker.defer_tracker_for_jit() as backward_flow_stats:
371                    loss, grads = jax.value_and_grad(loss_fn)(state.params)
372        else:
373            loss, grads = jax.value_and_grad(loss_fn)(state.params)
374        updates, opt_state = optimizer.update(grads, state.opt_state, state.params)
375        params = optax.apply_updates(state.params, updates)
376 
377        if ema_beta is None:
378            ema_params = None
379        else:
380            if state.ema_params is None:
381                raise ValueError("ema_params must be initialized when ema_beta is set.")
382            ema_params = jax.tree_util.tree_map(
383                lambda old, new: ema_beta * old + (1.0 - ema_beta) * new,
384                state.ema_params,
385                params,
386            )
387 
388        watch_stats = None
389        if watch_config is not None and compute_watch:
390            watch_stats = compute_watch_stats(
391                watch_targets=watch_targets,
392                include_norms=watch_config.include_norms,
393                include_per_parameter_norms=watch_config.include_per_parameter_norms,
394                include_histogram=watch_config.include_histograms,
395                split_scan_layers=watch_config.split_scan_layers,
396                params=state.params,
397                grads=grads,
398                updates=updates,
399                opt_state=state.opt_state,
400                model_tree_type=type(state.params),
401            )
402 
403        next_state = dataclasses.replace(
404            state,
405            step=state.step + one,
406            params=params,
407            opt_state=opt_state,
408            ema_params=ema_params,
409        )
410 
411        metrics = {"train/loss": loss}
412        if backward_flow_stats is not None:
413            metrics[_BACKWARD_FLOW_METRICS_KEY] = backward_flow_stats
414 
415        return next_state, metrics, watch_stats
416 
417    return train_step
418 
419 
420def _run_grug_local(config: GrugRunConfig) -> None:
421    """Entry point for the grug template training loop."""
422    trainer = config.trainer.trainer
423    trainer.initialize()
424    levanter.tracker.log_configuration(config)
425 
426    run_id = trainer.id
427    if run_id is None:
428        raise ValueError("trainer.id was not initialized")
429 
430    optimizer = config.optimizer.build(trainer.num_train_steps)
431    watch_config = trainer.watch
432    backward_flow_config = config.trainer.backward_flow if config.trainer.backward_flow.is_enabled else None
433    train_step = _make_train_step(
434        optimizer,
435        trainer.mp,
436        z_loss_weight=config.trainer.z_loss_weight,
437        loss_implementation=config.trainer.loss_implementation,
438        ema_beta=config.trainer.ema_beta,
439        watch_config=watch_config if watch_config.is_enabled else None,
440        backward_flow_config=backward_flow_config,
441    )
442 
443    data_key, model_key = jax.random.split(jax.random.PRNGKey(trainer.seed), 2)
444    if config.trainer.data_seed is not None:
445        data_key = jax.random.PRNGKey(config.trainer.data_seed)
446 
447    # Grug uses raw PartitionSpecs rather than Trainer's logical axis mapping.
448    # Keep the mesh compact so P(("replica_dcn", "data")) spans slices directly.
449    mesh = compact_grug_mesh()
450    checkpointer = trainer.checkpointer.create(run_id)
451    with set_mesh(mesh), TrainingDashboard(config, checkpointer.request_checkpoint, run_id):
452        batch_schedule = trainer.batch_schedule
453 
454        train_dataset = build_train_dataset(
455            config.data,
456            max_seq_len=config.model.max_seq_len,
457            batch_schedule=batch_schedule,
458            key=data_key,
459        )
460        train_loader = build_train_loader(
461            train_dataset,
462            batch_schedule=batch_schedule,
463            mesh=mesh,
464            batch_pspec=config.trainer.train_batch_pspec,
465        )
466 
467        @jax.jit
468        def _init_state(model_rng):
469            return initial_state(
470                config.model,
471                optimizer=optimizer,
472                mp=trainer.mp,
473                key=model_rng,
474                ema_beta=config.trainer.ema_beta,
475            )
476 
477        state = _init_state(model_key)
478 
479        state = restore_grug_state_from_checkpoint(
480            state,
481            checkpoint_search_paths=trainer.checkpoint_search_paths(run_id),
482            load_checkpoint_setting=trainer.load_checkpoint,
483            mesh=mesh,
484            allow_partial=trainer.allow_partial_checkpoint,
485        )
486        if trainer.initialize_from is not None:
487            state = init_weights_only_from_checkpoint(
488                state,
489                trainer.initialize_from,
490                mesh=mesh,
491                allow_partial=trainer.allow_partial_checkpoint,
492            )
493        dump_grug_state_sharding_run_artifact(
494            state,
495            log_dir=trainer.log_dir,
496            run_id=run_id,
497            path_override=config.trainer.sharding_dump_path,
498        )
499 
500        levanter.tracker.log_summary({"parameter_count": parameter_count(state.params)})
501 
502        flops_per_example, flops_summary = _compute_flops(model_config=config.model)
503        levanter.tracker.log_summary(flops_summary)
504 
505        eval_cfg = config.eval
506        evaluator = None
507        if eval_cfg is not None:
508            evaluator = build_tagged_evaluator(
509                data_config=config.data,
510                max_seq_len=config.model.max_seq_len,
511                mesh=mesh,
512                eval_cfg=eval_cfg,
513            )
514 
515        profiler_cfg = trainer.profiler
516        profiler_num_steps = profiler_cfg.resolve_num_profile_steps(num_train_steps=trainer.num_train_steps)
517        profiler_enabled = profiler_cfg.is_enabled and profiler_num_steps > 0
518 
519        log_every = max(1, config.trainer.log_every)
520        iterator = LoadingTimeTrackerIterator(train_loader.iter_from_step(int(state.step)))
521 
522        state_callbacks = StateCallbackRunner[GrugTrainState](
523            step_getter=lambda s: s.step,
524            model_getter=lambda s: s.params,
525            eval_model_getter=lambda s: s.ema_params if s.ema_params is not None else s.params,
526            opt_state_getter=lambda s: s.opt_state,
527        )
528        state_callbacks.add_hook(
529            callbacks.log_performance_stats(config.model.max_seq_len, batch_schedule, flops_per_example),
530            every=log_every,
531        )
532        state_callbacks.add_hook(callbacks.pbar_logger(total=trainer.num_train_steps), every=log_every)
533        state_callbacks.add_hook(callbacks.log_step_info(trainer.num_train_steps), every=log_every)
534        if profiler_enabled:
535            state_callbacks.add_hook(
536                profiler_cfg.build(
537                    str(trainer.log_dir / run_id / "profiler"),
538                    run_id=run_id,
539                    num_steps=profiler_num_steps,
540                ),
541                every=1,
542            )
543        state_callbacks.add_hook(_make_mixture_stage_callback(train_dataset, batch_schedule), every=1)
544        if evaluator is not None and eval_cfg is not None:
545            interval = eval_cfg.steps_per_eval
546            eval_ema = eval_cfg.eval_ema and config.trainer.ema_beta is not None
547            if interval is not None and interval > 0 and (eval_cfg.eval_current or eval_ema):
548                state_callbacks.add_hook(
549                    cb_tagged_evaluate(
550                        evaluator,
551                        prefix=eval_cfg.prefix,
552                        eval_current=eval_cfg.eval_current,
553                        eval_ema=eval_ema,
554                    ),
555                    every=interval,
556                )
557 
558        last_loss: float | jax.Array = 0.0
559        last_step_duration = 0.0
560        backward_flow_graph: BackwardFlowGraph | None = None
561 
562        # Main optimization loop.
563        try:
564            while int(state.step) < trainer.num_train_steps:
565                with jax.profiler.TraceAnnotation("load_batch"):
566                    batch = next(iterator)
567                step_start = time.perf_counter()
568                current_step = int(state.step)
569                # grad_watch runs only on its configured interval.
570                compute_watch = (
571                    watch_config.is_enabled and watch_config.interval > 0 and current_step % watch_config.interval == 0
572                )
573                compute_backward_flow = (
574                    backward_flow_config is not None
575                    and backward_flow_config.interval > 0
576                    and current_step % backward_flow_config.interval == 0
577                )
578                state, metrics, watch_stats = train_step(
579                    state,
580                    batch,
581                    compute_watch=compute_watch,
582                    compute_backward_flow=compute_backward_flow,
583                )
584                backward_flow_stats = metrics.get(_BACKWARD_FLOW_METRICS_KEY)
585                if backward_flow_stats is not None:
586                    metrics = {key: value for key, value in metrics.items() if key != _BACKWARD_FLOW_METRICS_KEY}
587                step = int(state.step) - 1
588 
589                jax.block_until_ready(metrics["train/loss"])
590                duration = time.perf_counter() - step_start
591                backward_flow_timing_metrics = None
592                if compute_backward_flow:
593                    backward_flow_timing_metrics = {"backward_flow/compute_step_duration": duration}
594 
595                hook_start = time.perf_counter()
596                with jax.profiler.TraceAnnotation("callbacks"):
597                    state_callbacks.run(state, loss=metrics["train/loss"], step_duration=duration)
598                    last_loss = metrics["train/loss"]
599                    last_step_duration = duration
600                    levanter.tracker.log({"throughput/hook_time": time.perf_counter() - hook_start}, step=step)
601                    levanter.tracker.log({"throughput/loading_time": iterator.this_load_time}, step=step)
602 
603                    if watch_stats is not None:
604                        levanter.tracker.log(watch_stats, step=step)
605                    if backward_flow_timing_metrics is not None:
606                        levanter.tracker.log(backward_flow_timing_metrics, step=step)
607                    if backward_flow_stats:
608                        levanter.tracker.log(backward_flow_stats, step=step)
609                        if backward_flow_graph is None:
610                            backward_flow_graph = _build_backward_flow_graph(
611                                params=state.params,
612                                batch=batch,
613                                mp=trainer.mp,
614                                z_loss_weight=config.trainer.z_loss_weight,
615                            )
616                        artifact_start = time.perf_counter()
617                        _write_backward_flow_artifact(
618                            graph=backward_flow_graph,
619                            backward_flow_metrics=backward_flow_stats,
620                            log_dir=trainer.log_dir,
621                            run_id=run_id,
622                            step=step,
623                        )
624                        levanter.tracker.log(
625                            {"backward_flow/artifact_write_duration": time.perf_counter() - artifact_start},
626                            step=step,
627                        )
628 
629                if checkpointer is not None:
630                    checkpointer.on_step(tree=state, step=int(state.step))
631        except BaseException:
632            logger.exception(
633                "Fatal error in grug training loop; skipping final callbacks/checkpoint to preserve root cause"
634            )
635            raise
636        else:
637            # Mirror classic trainer behavior: force callbacks on the last completed step.
638            state_callbacks.run(state, loss=last_loss, step_duration=last_step_duration, force=True)
639            if checkpointer is not None:
640                checkpointer.on_step(tree=state, step=int(state.step), force=True)
641                checkpointer.wait_until_finished()
642 
643    levanter.tracker.current_tracker().finish()
644 
645397
646def run_grug(config: GrugRunConfig) -> None:398def run_grug(config: GrugRunConfig) -> None:
n647    """Dispatch grug training through Fray jobs."""n399    """Dispatch pipeline-parallel Grug training through Fray."""
648    trainer = config.trainer.trainer400    if config.resources.replicas % config.train.physical_stages != 0:
649    if trainer.id is None:401        raise ValueError(
650        raise ValueError("trainer.id must be set before dispatching grug training.")402            f"training resource replicas ({config.resources.replicas}) must be divisible by "
651 403            f"{config.train.physical_stages} physical stages"
404        )
652    dispatch_grug_training_run(405    dispatch_grug_training_run(
n653        run_id=trainer.id,n406        run_id=config.run_id,
654        config=config,407        config=config.train,
655        local_entrypoint=_run_grug_local,408        local_entrypoint=_run_grug_local,
656        resources=config.resources,409        resources=config.resources,
nn410        extras=["pipeline"],
657    )411    )
t658 t
659 
660__all__ = [
661    "GrugEvalConfig",
662    "GrugRunConfig",
663    "GrugTrainState",
664    "GrugTrainerConfig",
665    "initial_state",
666    "run_grug",
667]

Back to top

README.md

added

experiments/grug/base/README.md
experiments/grug/moe_pipeline/README.md
tt1# grug-moe-pipeline
2 
3Canonical pipeline-parallel Grug MoE implementation. This directory is a copy-paste
4variant of [`experiments/grug/moe`](../moe/): its model and execution path are local
5so pipeline-specific changes do not turn the ordinary MoE implementation into a
6shared trainer framework.
7 
8[`pipeline.py`](./pipeline.py) splits the local transformer into stage pytrees and
9builds automatic JaxPP ZeroBubble or DualPipeV optimizer steps. [`train.py`](./train.py)
10owns model initialization, the training loop, and the Fray dispatch entry point.
11[`benchmark.py`](./benchmark.py) maps environment variables onto that trainer for
12repeatable performance runs. One logical stage maps to each physical pipeline rank with
13`PIPELINE_SCHEDULE=automatic_zero_bubble`. Set `PIPELINE_SCHEDULE=automatic_dualpipe_v`,
14`PIPELINE_STAGES=2P`, and `PIPELINE_PHYSICAL_STAGES=P` to fold two logical stages onto
15each physical rank in JaxPP's V-shaped placement. DualPipeV requires at least `2P`
16microbatches. `PIPELINE_LAYERS_PER_STAGE` contains one positive layer count per logical
17stage.
18 
19The canonical loop initializes fresh parameters, uses a fixed AdamW optimizer, and
20trains on synthetic token rows. It does not save or restore checkpoints. A model-specific
21training variant should replace those parts while retaining the stage split and JaxPP
22orchestration that it needs.
23 
24The best validated Snowball 67B-A2B throughput point uses eight H100x8 replicas,
25sixteen logical stages, batch 256, 32 microbatches, sequence length 8192, and layer
26counts `1,2,2,2,2,2,2,2,1,1,1,1,2,2,2,1`:
27 
28```bash
29export XLA_FLAGS='--xla_gpu_executable_terminate_timeout=300 --xla_gpu_enable_command_buffer='
30export PIPELINE_SCHEDULE=automatic_dualpipe_v
31export PIPELINE_PHYSICAL_STAGES=8
32export PIPELINE_STAGES=16
33export PIPELINE_BATCH=256
34export PIPELINE_MICROBATCHES=32
35export PIPELINE_SEQ_LEN=8192
36export PIPELINE_HIDDEN_DIM=2560
37export PIPELINE_INTERMEDIATE_DIM=1280
38export PIPELINE_SHARED_EXPERT_INTERMEDIATE_DIM=2560
39export PIPELINE_LAYERS=26
40export PIPELINE_LAYERS_PER_STAGE=1,2,2,2,2,2,2,2,1,1,1,1,2,2,2,1
41export PIPELINE_EXPERTS=256
42export PIPELINE_TOP_K=4
43export PIPELINE_EXPERT_AXIS=8
44export PIPELINE_HEADS=20
45export PIPELINE_KV_HEADS=5
46export PIPELINE_VOCAB_SIZE=128256
47export PIPELINE_SLIDING_WINDOW=2048
48export PIPELINE_QK_MULT=1.5703
49export PIPELINE_MP=params=bfloat16,compute=bfloat16,output=bfloat16
50export PIPELINE_ATTENTION=gpu_fa4_cute
51export PIPELINE_MOE=ring
52export PIPELINE_REMAT=recompute_all
53uv run --extra pipeline python -m experiments.grug.moe_pipeline.benchmark
54```
55 
56A 14-step run with four warmups measured 3.476 s median step time, 603,327 tokens/s,
5719.46% analytic MFU, and 59.153 GB peak memory per device. A matched batch-128,
5816-microbatch control measured 543,490 tokens/s and 17.53% analytic MFU. Both used
59eight examples per microbatch. The larger batch improved median throughput by 11.0%
60without increasing peak memory; final loss differed by 4.0e-5 after 14 synthetic-data
61steps.

Back to top

benchmark.py

added

experiments/grug/base/benchmark.py
experiments/grug/moe_pipeline/benchmark.py
tt1# Copyright The Marin Authors
2# SPDX-License-Identifier: Apache-2.0
3 
4"""Environment-configured benchmark for the Grug MoE pipeline trainer."""
5 
6import math
7import os
8from collections.abc import Mapping
9 
10from experiments.grug.moe_pipeline.train import GrugPipelineTrainConfig, PipelineSchedule, _run_grug_local
11 
12_ATTENTION_HEAD_DIM = 128
13 
14 
15def _env_int(environ: Mapping[str, str], name: str, default: int) -> int:
16    value = environ.get(name)
17    return default if not value else int(value)
18 
19 
20def _env_float(environ: Mapping[str, str], name: str, default: float) -> float:
21    value = environ.get(name)
22    return default if not value else float(value)
23 
24 
25def _default_num_kv_heads(num_heads: int) -> int:
26    for candidate in range(num_heads // 4, 0, -1):
27        if num_heads % candidate == 0:
28            return candidate
29    return 1
30 
31 
32def _resolve_benchmark_config(environ: Mapping[str, str]) -> GrugPipelineTrainConfig:
33    stages = _env_int(environ, "PIPELINE_STAGES", 4)
34    physical_stages = _env_int(environ, "PIPELINE_PHYSICAL_STAGES", stages)
35    seq_len = _env_int(environ, "PIPELINE_SEQ_LEN", 4096)
36    hidden_dim = _env_int(environ, "PIPELINE_HIDDEN_DIM", 2560)
37    warmup_steps = _env_int(environ, "PIPELINE_WARMUP_STEPS", 1)
38    profile_start_step = _env_int(environ, "PIPELINE_PROFILE_START_STEP", warmup_steps)
39    profile_steps = _env_int(environ, "PIPELINE_PROFILE_STEPS", 0)
40    profile_run_id = environ.get("PIPELINE_PROFILE_RUN_ID")
41    steps = _env_int(environ, "PIPELINE_STEPS", 4)
42    if profile_steps > 0:
43        if not profile_run_id:
44            raise ValueError("PIPELINE_PROFILE_RUN_ID is required when profiling")
45        if profile_start_step + profile_steps > steps:
46            raise ValueError("pipeline profile window must fit within PIPELINE_STEPS")
47 
48    layer_counts_value = environ.get("PIPELINE_LAYERS_PER_STAGE")
49    layer_counts = None if not layer_counts_value else tuple(int(value) for value in layer_counts_value.split(","))
50    memory_threshold_value = environ.get("PIPELINE_RESHARD_THRESHOLD_BYTES")
51    memory_threshold = None if not memory_threshold_value else int(memory_threshold_value)
52    if hidden_dim % _ATTENTION_HEAD_DIM != 0:
53        raise ValueError(f"PIPELINE_HIDDEN_DIM must be divisible by {_ATTENTION_HEAD_DIM}, got {hidden_dim}")
54    num_heads = hidden_dim // _ATTENTION_HEAD_DIM
55    num_kv_heads = _default_num_kv_heads(num_heads)
56    return GrugPipelineTrainConfig(
57        stages=stages,
58        physical_stages=physical_stages,
59        microbatches=_env_int(environ, "PIPELINE_MICROBATCHES", 8),
60        batch_size=_env_int(environ, "PIPELINE_BATCH", 256),
61        seq_len=seq_len,
62        hidden_dim=hidden_dim,
63        intermediate_dim=_env_int(environ, "PIPELINE_INTERMEDIATE_DIM", math.ceil(hidden_dim / 256) * 128),
64        shared_expert_intermediate_dim=_env_int(
65            environ,
66            "PIPELINE_SHARED_EXPERT_INTERMEDIATE_DIM",
67            hidden_dim,
68        ),
69        num_layers=_env_int(environ, "PIPELINE_LAYERS", 24),
70        num_experts=_env_int(environ, "PIPELINE_EXPERTS", 256),
71        top_k=_env_int(environ, "PIPELINE_TOP_K", 4),
72        expert_axis_size=_env_int(environ, "PIPELINE_EXPERT_AXIS", 8),
73        vocab_size=_env_int(environ, "PIPELINE_VOCAB_SIZE", 128_256),
74        num_heads=_env_int(environ, "PIPELINE_HEADS", num_heads),
75        num_kv_heads=_env_int(environ, "PIPELINE_KV_HEADS", num_kv_heads),
76        sliding_window=_env_int(environ, "PIPELINE_SLIDING_WINDOW", 2048),
77        qk_mult=_env_float(environ, "PIPELINE_QK_MULT", 1.3),
78        steps=steps,
79        warmup_steps=warmup_steps,
80        profile_start_step=profile_start_step,
81        profile_steps=profile_steps,
82        profile_run_id=profile_run_id,
83        layer_counts=layer_counts,
84        memory_threshold=memory_threshold,
85        mp_policy_string=environ.get("PIPELINE_MP", "params=bfloat16,compute=bfloat16,output=bfloat16"),
86        remat_mode=environ.get("PIPELINE_REMAT", "recompute_all"),
87        attention_implementation=environ.get("PIPELINE_ATTENTION", "gpu_fa4_cute"),
88        moe_implementation=environ.get("PIPELINE_MOE", "ring"),
89        schedule=PipelineSchedule(environ.get("PIPELINE_SCHEDULE", PipelineSchedule.ZERO_BUBBLE)),
90    )
91 
92 
93def main() -> None:
94    config = _resolve_benchmark_config(os.environ)
95    _run_grug_local(config)
96 
97 
98if __name__ == "__main__":
99    main()

Back to top

pipeline.py

added

experiments/grug/base/pipeline.py
experiments/grug/moe_pipeline/pipeline.py
tt1# Copyright The Marin Authors
2# SPDX-License-Identifier: Apache-2.0
3 
4"""Automatic pipeline-parallel training for the Grug MoE pipeline variant."""
5 
6from __future__ import annotations
7 
8import dataclasses
9import itertools
10from collections.abc import Callable
11from dataclasses import dataclass
12from enum import StrEnum
13from typing import TypeGuard
14 
15import equinox as eqx
16import jax
17import jax.numpy as jnp
18import jmp
19import numpy as np
20import optax
21from haliax.jax_utils import named_call
22from jax.sharding import AxisType, Mesh, NamedSharding
23from jax.sharding import PartitionSpec as P
24from jax.tree_util import register_dataclass
25from jaxtyping import Array, Float, Int
26from levanter.data.text.examples import GrugLmExample
27from levanter.grug.attention import AttentionMask
28from levanter.grug.grug_moe import MOE_REMAT_SAVE_NAMES
29from levanter.grug.loss import fused_linear_softmax_cross_entropy_loss
30from levanter.pipeline import evenly_partition_layers
31 
32from experiments.grug.moe_pipeline.model import (
33    BATCH_AXES,
34    Block,
35    GatedNorm,
36    GrugModelConfig,
37    RMSNorm,
38    Transformer,
39)
40 
41try:
42    import jaxpp.api as jaxpp
43except ModuleNotFoundError as error:
44    if error.name != "jaxpp":
45        raise
46    jaxpp = None
47 
48if jaxpp is None:
49    mpmd = None
50else:
51    from jaxpp.experimental import mpmd
52 
53 
54TRAIN_LOSS_KEY = "train/loss"
55_QB_BETA_PER_LAYER_KEY = "qb_beta_per_layer"
56_PIPELINE_AXIS = "pipeline"
57 
58type _ArrayValue = jax.Array | jax.ShapeDtypeStruct | jaxpp.MpmdArray
59 
60 
61class AutomaticPipelineSchedule(StrEnum):
62    ZERO_BUBBLE = "zero_bubble"
63    DUALPIPE_V = "dualpipe_v"
64 
65 
66@dataclass(frozen=True)
67class GrugMoePipelineConfig:
68    stages: int
69    microbatches: int
70    physical_stages: int | None = None
71 
72    def __post_init__(self) -> None:
73        if self.stages < 2:
74            raise ValueError(f"pipeline parallelism requires at least 2 stages, got {self.stages}")
75        if self.microbatches <= 0:
76            raise ValueError(f"microbatches must be positive, got {self.microbatches}")
77        if self.physical_stages is not None:
78            if self.physical_stages < 2:
79                raise ValueError(f"pipeline parallelism requires at least 2 physical stages, got {self.physical_stages}"
 >)
80            if self.stages != 2 * self.physical_stages:
81                raise ValueError(
82                    "virtual pipeline parallelism requires exactly two logical stages per physical stage; "
83                    f"got {self.stages} logical and {self.physical_stages} physical stages"
84                )
85 
86    @property
87    def mpmd_stages(self) -> int:
88        return self.stages if self.physical_stages is None else self.physical_stages
89 
90 
91def make_pipeline_mesh(
92    config: GrugMoePipelineConfig,
93    *,
94    expert_axis_size: int,
95    replica_axis_size: int | None = None,
96):
97    """Build the concrete Grug mesh and wrap it as a JaxPP MPMD mesh."""
98    pp, _ = _jaxpp_modules()
99    if replica_axis_size is None:
100        replica_axis_size = max(1, jax.process_count() // config.mpmd_stages)
101    fixed_axes = config.mpmd_stages * replica_axis_size * expert_axis_size
102    if jax.device_count() % fixed_axes != 0:
103        raise ValueError(
104            f"device count {jax.device_count()} must be divisible by stages ({config.mpmd_stages}) * "
105            f"replicas ({replica_axis_size}) * experts ({expert_axis_size})"
106        )
107 
108    data_axis_size = jax.device_count() // fixed_axes
109    shape = (config.mpmd_stages, replica_axis_size, data_axis_size, expert_axis_size, 1)
110    axis_names = (_PIPELINE_AXIS, *BATCH_AXES, "model")
111    devices = np.asarray(jax.devices(), dtype=object).reshape(shape)
112    mesh = Mesh(devices, axis_names, axis_types=(AxisType.Explicit,) * len(axis_names))
113    if mesh.is_multi_process:
114        local_stages = {int(np.argwhere(devices == device)[0][0]) for device in jax.local_devices()}
115        if len(local_stages) != 1:
116            raise ValueError(f"each JAX process must own exactly one pipeline stage; got {sorted(local_stages)}")
117    return mesh, pp.MpmdMesh(mesh, _PIPELINE_AXIS)
118 
119 
120class GrugMoePipelineStage(eqx.Module):
121    """The parameters and layer range owned by one pipeline stage."""
122 
123    token_embed: jax.Array | None
124    embed_norm: RMSNorm | None
125    embed_gated_norm: GatedNorm | None
126    output_proj: jax.Array | None
127    blocks: tuple[Block, ...]
128    final_norm: RMSNorm | None
129    final_gated_norm: GatedNorm | None
130    config: GrugModelConfig = eqx.field(static=True)
131    start_layer: int = eqx.field(static=True)
132    end_layer: int = eqx.field(static=True)
133 
134    @named_call
135    def embed(self, token_ids: Int[Array, "B S"]) -> Float[Array, "B S D"]:
136        if self.token_embed is None or self.embed_norm is None or self.embed_gated_norm is None:
137            raise ValueError("only stage 0 owns the token embedding")
138        hidden = self.token_embed.at[token_ids].get(out_sharding=P(BATCH_AXES))
139        return self.embed_gated_norm(self.embed_norm(hidden))
140 
141    @named_call
142    def run_blocks(
143        self,
144        hidden: Float[Array, "B S D"],
145        mask: AttentionMask | jax.Array | None,
146    ) -> tuple[Float[Array, "B S D"], dict[str, jax.Array]]:
147        if mask is None:
148            mask = AttentionMask.causal()
149 
150        cfg = self.config
151        segment_ids = mask.segment_ids if isinstance(mask, AttentionMask) else None
152        short_mask = AttentionMask(is_causal=True, sliding_window=cfg.sliding_window, segment_ids=segment_ids)
153        long_mask = AttentionMask(is_causal=True, sliding_window=None, segment_ids=segment_ids)
154        remat_policy = (
155            jax.checkpoint_policies.save_only_these_names(*MOE_REMAT_SAVE_NAMES)
156            if cfg.remat_mode == "save_moe"
157            else None
158        )
159 
160        block_metrics = []
161        for local_index, block in enumerate(self.blocks):
162            layer_index = self.start_layer + local_index
163            is_long = layer_index % 4 == 3 or layer_index == cfg.num_layers - 1
164            layer_mask = long_mask if is_long else short_mask
165            hidden, metrics = eqx.filter_checkpoint(block, policy=remat_policy)(
166                hidden,
167                layer_mask,
168                use_pko=is_long and not cfg.disable_pko,
169                disable_rope=is_long and cfg.disable_long_rope,
170            )
171            block_metrics.append(metrics)
172 
173        return hidden, _stack_router_metrics(block_metrics)
174 
175    @named_call
176    def finish(self, hidden: Float[Array, "B S D"]) -> Float[Array, "B S D"]:
177        if self.final_norm is None or self.final_gated_norm is None:
178            raise ValueError("only the final stage owns the final norms")
179        return self.final_gated_norm(self.final_norm(hidden))
180 
181    @named_call
182    def cross_entropy_loss(
183        self,
184        hidden: Float[Array, "B S D"],
185        token_ids: Int[Array, "B S"],
186        loss_weight: Float[Array, "B S"],
187        *,
188        logsumexp_weight: float | None,
189        reduction: str = "mean",
190    ) -> jax.Array:
191        if self.output_proj is None:
192            raise ValueError("only the final stage owns the output projection")
193        labels = jnp.pad(token_ids[:, 1:], ((0, 0), (0, 1))).astype(jnp.int32)
194        return fused_linear_softmax_cross_entropy_loss(
195            hidden,
196            self.output_proj,
197            labels,
198            weight=loss_weight.astype(jnp.float32),
199            reduction=reduction,
200            logsumexp_weight=logsumexp_weight,
201            dtype=jnp.float32,
202        )
203 
204    def local_router_loss(self, router_metrics: dict[str, jax.Array]) -> jax.Array:
205        coefficient = jnp.asarray(self.config.router_z_loss_coef / self.config.num_layers, dtype=jnp.float32)
206        return coefficient * jnp.sum(router_metrics["router_z_loss_per_layer"])
207 
208 
209@register_dataclass
210@dataclass(frozen=True)
211class GrugMoeAutomaticPipelineState:
212    """Array state for JaxPP's automatic pipeline transform."""
213 
214    trainable_params: tuple[GrugMoePipelineStage, ...]
215    opt_state: tuple[optax.OptState, ...]
216    pending_qb_betas: tuple[jax.Array, ...]
217 
218 
219@dataclass(frozen=True)
220class _InitializedMpmdStageState:
221    opt_state: tuple[optax.OptState, ...]
222    pending_qb_betas: tuple[jax.Array, ...]
223 
224 
225@dataclass(frozen=True)
226class PreparedAutomaticMpmdStep:
227    step: Callable[..., tuple[GrugMoeAutomaticPipelineState, dict[str, jax.Array]]]
228    state: GrugMoeAutomaticPipelineState
229    batches: GrugLmExample
230    loss_denominator: jax.Array
231 
232 
233def split_transformer(
234    model: Transformer,
235    num_stages: int,
236    *,
237    layer_counts: tuple[int, ...] | None = None,
238) -> tuple[GrugMoePipelineStage, ...]:
239    """Split a Grug MoE transformer into contiguous stage pytrees."""
240    if layer_counts is None:
241        ranges = evenly_partition_layers(len(model.blocks), num_stages)
242    else:
243        if len(layer_counts) != num_stages:
244            raise ValueError(f"expected {num_stages} layer counts, got {len(layer_counts)}")
245        if any(count <= 0 for count in layer_counts):
246            raise ValueError(f"layer counts must be positive, got {layer_counts}")
247        if sum(layer_counts) != len(model.blocks):
248            raise ValueError(f"layer counts sum to {sum(layer_counts)}, but model has {len(model.blocks)} layers")
249        boundaries = np.cumsum((0, *layer_counts))
250        ranges = tuple((int(start), int(end)) for start, end in itertools.pairwise(boundaries))
251    stages = []
252    for stage_index, (start_layer, end_layer) in enumerate(ranges):
253        is_first = stage_index == 0
254        is_last = stage_index == num_stages - 1
255        stages.append(
256            GrugMoePipelineStage(
257                token_embed=model.token_embed if is_first else None,
258                embed_norm=model.embed_norm if is_first else None,
259                embed_gated_norm=model.embed_gated_norm if is_first else None,
260                output_proj=model.output_proj if is_last else None,
261                blocks=model.blocks[start_layer:end_layer],
262                final_norm=model.final_norm if is_last else None,
263                final_gated_norm=model.final_gated_norm if is_last else None,
264                config=model.config,
265                start_layer=start_layer,
266                end_layer=end_layer,
267            )
268        )
269    return tuple(stages)
270 
271 
272def split_automatic_stages(
273    model: Transformer,
274    *,
275    num_stages: int,
276    layer_counts: tuple[int, ...] | None = None,
277) -> tuple[tuple[GrugMoePipelineStage, ...], tuple[GrugMoePipelineStage, ...]]:
278    """Partition automatic-schedule stages into trainable and static pytrees.
279 
280    Returns:
281        The trainable and static stage tuples. Router biases are removed from
282        both because the pending QB update supplies them separately.
283    """
284    stages = split_transformer(model, num_stages, layer_counts=layer_counts)
285    trainable_stages = []
286    static_stages = []
287    for stage in stages:
288        trainable_stage, static_stage = eqx.partition(stage, eqx.is_array)
289        for block_index in range(len(stage.blocks)):
290            trainable_stage = eqx.tree_at(
291                lambda current, index=block_index: current.blocks[index].mlp.router_bias,
292                trainable_stage,
293                None,
294            )
295        trainable_stages.append(trainable_stage)
296        static_stages.append(static_stage)
297    return tuple(trainable_stages), tuple(static_stages)
298 
299 
300def _process_has_sharding(sharding: NamedSharding) -> bool:
301    process_index = jax.process_index()
302    return any(device.process_index == process_index for device in sharding.mesh.devices.flat)
303 
304 
305def _empty_sharded_array(shape: tuple[int, ...], dtype, sharding: NamedSharding) -> jax.Array:
306    return jax.make_array_from_single_device_arrays(shape, sharding, [], dtype=dtype)
307 
308 
309def _stage_local_scalar(value: jax.Array, sharding: NamedSharding) -> jax.Array:
310    if not _process_has_sharding(sharding):
311        return _empty_sharded_array((), value.dtype, sharding)
312    return jax.device_put(np.asarray(value), sharding)
313 
314 
315def _localize_optimizer_scalars(mpmd_mesh, stage_index: int, opt_state):
316    stage_mesh = mpmd_mesh.unstack[stage_index]
317 
318    def localize(value):
319        if _is_array(value) and value.shape == ():
320            return _stage_local_scalar(value, NamedSharding(stage_mesh, P()))
321        return value
322 
323    return jax.tree.map(localize, opt_state)
324 
325 
326def _initialize_mpmd_stage_state(
327    stages: tuple[GrugMoePipelineStage, ...],
328    optimizer: optax.GradientTransformation,
329    mpmd_mesh,
330    stage_to_mpmd_index: tuple[int, ...],
331    memory_threshold: int | None,
332) -> _InitializedMpmdStageState:
333    pp, _ = _jaxpp_modules()
334    qb_targets = tuple(
335        pp.MpmdSharding(mpmd_mesh, mesh_ids={mpmd_index}, spec=P(None, None)) for mpmd_index in stage_to_mpmd_index
336    )
337    pending_qb_betas = pp.spmd_to_mpmd_reshard(
338        mpmd_mesh,
339        tuple(jnp.zeros((len(stage.blocks), stage.config.num_experts), dtype=jnp.float32) for stage in stages),
340        qb_targets,
341        threshold=memory_threshold,
342    )
343    opt_state = tuple(
344        _localize_optimizer_scalars(mpmd_mesh, mpmd_index, optimizer.init(stage))
345        for mpmd_index, stage in zip(stage_to_mpmd_index, stages, strict=True)
346    )
347    return _InitializedMpmdStageState(
348        opt_state=opt_state,
349        pending_qb_betas=pending_qb_betas,
350    )
351 
352 
353def initialize_mpmd_automatic_pipeline_state(
354    model: Transformer,
355    optimizer: optax.GradientTransformation,
356    mpmd_mesh,
357    *,
358    num_stages: int,
359    layer_counts: tuple[int, ...] | None = None,
360    stage_to_mpmd_index: tuple[int, ...] | None = None,
361    memory_threshold: int | None = None,
362) -> tuple[GrugMoeAutomaticPipelineState, tuple[GrugMoePipelineStage, ...]]:
363    """Return automatic state and static stages using the requested placement."""
364    pp, _ = _jaxpp_modules()
365    if stage_to_mpmd_index is None:
366        stage_to_mpmd_index = tuple(range(num_stages))
367    if len(stage_to_mpmd_index) != num_stages:
368        raise ValueError(f"expected {num_stages} stage placements, got {len(stage_to_mpmd_index)}")
369    trainable_stages, static_stages = split_automatic_stages(
370        model,
371        num_stages=num_stages,
372        layer_counts=layer_counts,
373    )
374    trainable_targets = tuple(
375        _mpmd_sharding_tree(mpmd_mesh, mpmd_index, stage)
376        for mpmd_index, stage in zip(stage_to_mpmd_index, trainable_stages, strict=True)
377    )
378    trainable_stages = pp.spmd_to_mpmd_reshard(
379        mpmd_mesh,
380        trainable_stages,
381        trainable_targets,
382        threshold=memory_threshold,
383    )
384    initialized = _initialize_mpmd_stage_state(
385        trainable_stages,
386        optimizer,
387        mpmd_mesh,
388        stage_to_mpmd_index,
389        memory_threshold,
390    )
391    return (
392        GrugMoeAutomaticPipelineState(
393            trainable_params=trainable_stages,
394            opt_state=initialized.opt_state,
395            pending_qb_betas=initialized.pending_qb_betas,
396        ),
397        static_stages,
398    )
399 
400 
401def _stack_router_metrics(block_metrics: list[dict[str, jax.Array]]) -> dict[str, jax.Array]:
402    keys = (
403        "routing_entropy",
404        "routing_counts",
405        "load_balancing_loss",
406        "router_z_loss",
407        "qb_beta",
408        "capacity_overflow",
409    )
410    return {f"{key}_per_layer": jnp.stack([metrics[key] for metrics in block_metrics]) for key in keys}
411 
412 
413def _apply_qb_betas(stage: GrugMoePipelineStage, qb_betas: jax.Array) -> GrugMoePipelineStage:
414    blocks = list(stage.blocks)
415    for index, block in enumerate(blocks):
416        new_bias = -qb_betas[index]
417        new_bias = new_bias - jnp.mean(new_bias)
418        new_mlp = eqx.tree_at(
419            lambda mlp: mlp.router_bias,
420            block.mlp,
421            new_bias,
422            is_leaf=lambda value: value is None,
423        )
424        blocks[index] = eqx.tree_at(lambda current: current.mlp, block, new_mlp)
425    return eqx.tree_at(lambda current: current.blocks, stage, tuple(blocks))
426 
427 
428def _jaxpp_modules():
429    if jaxpp is None or mpmd is None:
430        raise ModuleNotFoundError("The canonical Grug pipeline requires `uv sync --extra pipeline`.")
431    return jaxpp, mpmd
432 
433 
434def automatic_stage_to_mpmd_indices(
435    config: GrugMoePipelineConfig,
436    schedule_name: AutomaticPipelineSchedule,
437) -> tuple[int, ...]:
438    """Return the physical MPMD rank that owns each logical automatic stage."""
439    schedule = _automatic_schedule(config, schedule_name)
440    return tuple(int(schedule.get_mpmd_idx(stage_index)) for stage_index in range(config.stages))
441 
442 
443def _automatic_schedule(config: GrugMoePipelineConfig, schedule_name: AutomaticPipelineSchedule):
444    pp, _ = _jaxpp_modules()
445    if schedule_name == AutomaticPipelineSchedule.ZERO_BUBBLE:
446        return pp.ZeroBubble(num_stages=config.stages)
447    if schedule_name == AutomaticPipelineSchedule.DUALPIPE_V:
448        return pp.DualPipeV(num_stages=config.stages, mpmd_dim=config.mpmd_stages)
449    raise ValueError(f"unknown automatic pipeline schedule: {schedule_name}")
450 
451 
452def _is_array(value: object) -> TypeGuard[_ArrayValue]:
453    if isinstance(value, (jax.Array, jax.ShapeDtypeStruct)):
454        return True
455    return jaxpp is not None and isinstance(value, jaxpp.MpmdArray)
456 
457 
458def _partition_spec_tree(tree):
459    def partition_spec(value):
460        if not _is_array(value):
461            return None
462        if isinstance(value.sharding, NamedSharding):
463            return value.sharding.spec
464        if jaxpp is not None and isinstance(value.sharding, jaxpp.MpmdSharding):
465            return value.sharding.spec
466        return P(*([None] * value.ndim))
467 
468    return jax.tree.map(partition_spec, tree)
469 
470 
471def _mpmd_sharding_tree(mpmd_mesh, stage_index: int, tree):
472    pp, _ = _jaxpp_modules()
473 
474    def sharding(value):
475        if not _is_array(value):
476            return None
477        spec = value.sharding.spec if isinstance(value.sharding, NamedSharding) else P(*([None] * value.ndim))
478        return pp.MpmdSharding(mpmd_mesh, mesh_ids={stage_index}, spec=spec)
479 
480    return jax.tree.map(sharding, tree)
481 
482 
483def make_automatic_pipeline_step(
484    optimizer: optax.GradientTransformation,
485    mp_policy: jmp.Policy,
486    static_stages: tuple[GrugMoePipelineStage, ...],
487    sample_state: GrugMoeAutomaticPipelineState,
488    sample_batches: GrugLmExample,
489    *,
490    config: GrugMoePipelineConfig,
491    mpmd_mesh,
492    schedule_name: AutomaticPipelineSchedule = AutomaticPipelineSchedule.ZERO_BUBBLE,
493    logsumexp_weight: float | None = None,
494):
495    """Build a JaxPP automatic ZeroBubble or DualPipeV optimizer step.
496 
497    ``static_stages`` and ``sample_state`` must come from the same automatic
498    state initializer. ``sample_batches`` must have the leading microbatch axis
499    produced by :func:`levanter.pipeline.reshape_batch_into_microbatches`.
500 
501    """
502    pp, _ = _jaxpp_modules()
503    schedule = _automatic_schedule(config, schedule_name)
504 
505    def pipeline_step(
506        state: GrugMoeAutomaticPipelineState,
507        batches: GrugLmExample,
508        loss_denominator: jax.Array,
509    ):
510        def loss_fn(trainable_stages: tuple[GrugMoePipelineStage, ...], batch: GrugLmExample):
511            hidden = None
512            router_loss = jnp.array(0.0, dtype=jnp.float32)
513            next_qb_betas = []
514            last_stage = None
515            for stage_index, (trainable_stage, static_stage) in enumerate(
516                zip(trainable_stages, static_stages, strict=True)
517            ):
518                stage = eqx.combine(trainable_stage, static_stage)
519                stage = mp_policy.cast_to_compute(_apply_qb_betas(stage, state.pending_qb_betas[stage_index]))
520                if stage_index == 0:
521                    hidden = stage.embed(batch.tokens)
522                assert hidden is not None
523                hidden, router_metrics = stage.run_blocks(hidden, batch.attn_mask)
524                router_loss = router_loss + stage.local_router_loss(router_metrics) / config.microbatches
525                next_qb_betas.append(router_metrics[_QB_BETA_PER_LAYER_KEY])
526                if stage_index < config.stages - 1:
527                    hidden = pp.mark_stage_end(hidden)
528                last_stage = stage
529 
530            assert last_stage is not None
531            hidden = last_stage.finish(hidden)
532            cross_entropy_sum = last_stage.cross_entropy_loss(
533                hidden,
534                batch.tokens,
535                batch.loss_weight,
536                logsumexp_weight=logsumexp_weight,
537                reduction="sum",
538            )
539            loss = cross_entropy_sum / loss_denominator + router_loss
540            loss = pp.mark_stage_end(loss)
541            return loss, tuple(next_qb_betas)
542 
543        (loss, next_qb_betas), grads = pp.treduce(
544            lambda batch: jax.value_and_grad(loss_fn, has_aux=True)(state.trainable_params, batch),
545            batches,
546            schedule=schedule,
547            operation=((pp.Add, tuple(pp.Add for _ in range(config.stages))), pp.Add),
548        )
549        next_params = []
550        next_opt_state = []
551        for params, opt_state, stage_grads in zip(
552            state.trainable_params,
553            state.opt_state,
554            grads,
555            strict=True,
556        ):
557            updates, stage_opt_state = optimizer.update(stage_grads, opt_state, params)
558            next_params.append(eqx.apply_updates(params, updates))
559            next_opt_state.append(stage_opt_state)
560        next_state = dataclasses.replace(
561            state,
562            trainable_params=tuple(next_params),
563            opt_state=tuple(next_opt_state),
564            pending_qb_betas=tuple(beta / config.microbatches for beta in next_qb_betas),
565        )
566        return next_state, {TRAIN_LOSS_KEY: loss}
567 
568    return pp.mpmd_jit_with_loop(
569        pipeline_step,
570        mpmd_mesh=mpmd_mesh,
571        in_specs=(_partition_spec_tree(sample_state), _partition_spec_tree(sample_batches), P()),
572        out_specs=(_partition_spec_tree(sample_state), {TRAIN_LOSS_KEY: P()}),
573    )
574 
575 
576def prepare_automatic_mpmd_step(
577    step,
578    state: GrugMoeAutomaticPipelineState,
579    batches: GrugLmExample,
580    loss_denominator: jax.Array,
581    mpmd_mesh,
582    *,
583    memory_threshold: int | None = None,
584) -> PreparedAutomaticMpmdStep:
585    """Compile with stage-local state and place the remaining SPMD inputs."""
586    pp, _ = _jaxpp_modules()
587    compiled = step.compile(state, batches, loss_denominator)
588    args_shardings, kwargs_shardings = compiled.in_shardings
589    if kwargs_shardings:
590        raise ValueError("automatic pipeline step does not accept keyword arguments")
591 
592    def place_initial_scalar(value, target):
593        if not _is_array(value) or isinstance(value, pp.MpmdArray):
594            return value
595        if value.shape != ():
596            return value
597        mesh_ids = target.mesh_ids or {0}
598        local_arrays = []
599        for stage_index in sorted(mesh_ids):
600            sharding = NamedSharding(mpmd_mesh.unstack[stage_index], target.spec)
601            if _process_has_sharding(sharding):
602                local_arrays.append(jax.device_put(np.zeros((), dtype=value.dtype), sharding))
603        return pp.MpmdArray(
604            local_arrays,
605            target,
606            shape=(),
607            dtype=value.dtype,
608        )
609 
610    state = jax.tree.map(place_initial_scalar, state, args_shardings[0])
611    if memory_threshold is None and all(device.memory_stats() is None for device in jax.local_devices()):
612        memory_threshold = 0
613    batches, loss_denominator = pp.spmd_to_mpmd_reshard(
614        mpmd_mesh,
615        (batches, loss_denominator),
616        (args_shardings[1], args_shardings[2]),
617        threshold=memory_threshold,
618    )
619    return PreparedAutomaticMpmdStep(
620        step=compiled,
621        state=state,
622        batches=batches,
623        loss_denominator=loss_denominator,
624    )

Back to top

launch.py

removed

experiments/grug/base/launch.py
experiments/grug/moe_pipeline/launch.py
t1# Copyright The Marin Authorst
2# SPDX-License-Identifier: Apache-2.0
3 
4"""grug-base trial run, authored as a lazy artifact.
5 
6The run is a function that returns a typed :class:`Checkpoint` handle addressed by an
7explicit ``name@version``. The model, optimizer, data mixture, token budget, and evals
8are stated inline; the output path is ``ctx.output_path`` and the TPU is a run-arg, so neither
9bears on the artifact's identity.
10 
11The grug training mechanism (``GrugBaseLaunchConfig`` + ``build_grug_run_config`` ->
12``run_grug``) is grug-specific compute and is kept as-is: the recipe's ``fn`` builds the
13Levanter trainer (its checkpointer paths derive from ``output_path``) and dispatches the
14training job to Fray. Only the data/validation wiring around it is assembled lazily.
15"""
16 
17import dataclasses
18import os
19from dataclasses import dataclass
20from datetime import timedelta
21 
22import jmp
23from fray.cluster import ResourceConfig
24from levanter.callbacks.profiler import ProfilerConfig
25from levanter.checkpoint import CheckpointerConfig
26from levanter.data.text.datasets import LmDataConfig
27from levanter.optim.config import AdamConfig, OptimizerConfig
28from levanter.tracker import TrackerConfig
29from levanter.tracker.wandb import WandbConfig
30from levanter.trainer import TrainerConfig
31from marin.execution.build_context import resolve_version
32from marin.execution.lazy import ArtifactStep, StepContext
33from marin.experiment.cli import experiment_main
34from marin.experiment.data import mixture
35from marin.experiment.namespacing import user_namespaced_name
36from marin.training.training import LevanterCheckpoint, resolve_checkpointer_output_path
37 
38from experiments.datasets.nemotron import nemotron_datasets
39from experiments.datasets.paloma import paloma_datasets
40from experiments.datasets.proofpile import proofpile_dataset
41from experiments.datasets.starcoder import starcoder_dataset
42from experiments.datasets.uncheatable import uncheatable_datasets
43from experiments.grug.base.model import GrugModelConfig
44from experiments.grug.base.train import GrugEvalConfig, GrugRunConfig, GrugTrainerConfig, run_grug
45from experiments.llama import llama3_tokenizer
46 
47# The TPU the training job is dispatched onto. A run-arg, not part of the config's
48# identity: re-running on a different TPU is the same checkpoint. The launcher step
49# runs inline (run_grug dispatches its own Fray job).
50_TRAIN_RESOURCES = ResourceConfig.with_tpu("v5p-8")
51 
52# Nemotron CC mixture weights: the corpus's TiB proportions, plus starcoder and
53# proof-pile at their published weights. Policy lives here, in the experiment.
54_NEMOTRON_WEIGHTS = {
55    "hq_actual": 0.91351,
56    "hq_synth": 2.72,
57    "medium_high": 0.82471,
58    "medium": 3.38,
59    "medium_low": 1.54,
60    "low_actual": 0.70123,
61    "low_synth": 0.62771,
62}
63_STARCODER_WEIGHT = 0.25
64_PROOFPILE_WEIGHT = 0.055
65 
66 
67GRUG_130M_MODEL = GrugModelConfig(
68    vocab_size=128_256,
69    hidden_dim=512,
70    intermediate_dim=1792,
71    num_layers=6,
72    num_heads=8,
73    num_kv_heads=8,
74    max_seq_len=4096,
75    head_dim=None,
76)
77 
78 
79@dataclass(frozen=True)
80class GrugBaseLaunchConfig:
81    """Last-mile run config for the base grug template.
82 
83    Keep this as the main entry point for day-to-day edits (model/data/optimizer/trainer/eval knobs).
84 
85    The trainer and eval knobs are flat scalars rather than nested
86    ``GrugTrainerConfig`` / ``GrugEvalConfig`` objects: those carry
87    ``jax.sharding.PartitionSpec`` batch-sharding defaults, which the artifact
88    fingerprint cannot serialize. ``build_grug_run_config`` reconstitutes them (with
89    their default pspecs) at run time, keeping the sharding plumbing out of the
90    artifact's identity.
91    """
92 
93    model: GrugModelConfig
94    data: LmDataConfig
95    output_path: str
96    run_id: str
97    resources: ResourceConfig
98    steps: int
99    batch_size: int
100    seed: int
101    mp: str  # jmp policy string, e.g. "params=float32,compute=bfloat16,output=bfloat16".
102    tracker: TrackerConfig
103    optimizer: OptimizerConfig
104    z_loss_weight: float = 1e-4
105    ema_beta: float | None = None  # EMA coefficient for eval/checkpoint model; None disables EMA.
106    log_every: int = 1
107    loss_implementation: str | tuple[str, ...] | None = None  # cross-entropy kernel; None uses the trainer default.
108    eval_batch_size: int | None = 512  # None disables perplexity eval.
109    steps_per_eval: int = 1000
110    max_eval_batches: int = 8
111    eval_current: bool = True
112    eval_ema: bool = False
113 
114 
115def _resolve_run_id(default_run_id: str) -> str:
116    """Resolve run id and append `FERRY_DATE` when launching from ferry workflows."""
117    run_id = os.environ.get("GRUG_RUN_ID", default_run_id)
118    ferry_date = os.environ.get("FERRY_DATE")
119    if ferry_date:
120        run_id = f"{run_id}-{ferry_date}"
121    return run_id
122 
123 
124def _resolve_tracker(tracker: TrackerConfig, run_id: str) -> TrackerConfig:
125    if isinstance(tracker, WandbConfig):
126        return dataclasses.replace(tracker, name=run_id)
127    return tracker
128 
129 
130def build_grug_run_config(launch: GrugBaseLaunchConfig) -> GrugRunConfig:
131    """Map launch knobs onto the trainer's full ``GrugRunConfig``.
132 
133    The checkpointer's ``base_path`` and ``temporary_base_path`` derive from
134    ``launch.output_path``, so a run resolves its checkpoint locations under the
135    region its output path lives in.
136    """
137    output_path = launch.output_path
138    trainer = TrainerConfig(
139        id=launch.run_id,
140        seed=launch.seed,
141        train_batch_size=launch.batch_size,
142        num_train_steps=launch.steps,
143        profiler=ProfilerConfig(enabled=False, start_step=5, num_steps=100, perfetto_link=False),
144        mp=jmp.get_policy(launch.mp),
145        tracker=_resolve_tracker(launch.tracker, launch.run_id),
146        use_explicit_mesh_axes=True,
147        require_accelerator=True,
148        allow_nondivisible_batch_size=False,
149        checkpointer=resolve_checkpointer_output_path(
150            CheckpointerConfig(save_interval=timedelta(minutes=10), keep=None),
151            output_path,
152        ),
153    )
154 
155    grug_trainer = GrugTrainerConfig(
156        trainer=trainer,
157        z_loss_weight=launch.z_loss_weight,
158        ema_beta=launch.ema_beta,
159        log_every=launch.log_every,
160        loss_implementation=launch.loss_implementation,
161    )
162 
163    eval_config = (
164        GrugEvalConfig(
165            eval_batch_size=launch.eval_batch_size,
166            steps_per_eval=launch.steps_per_eval,
167            max_eval_batches=launch.max_eval_batches,
168            eval_current=launch.eval_current,
169            eval_ema=launch.eval_ema,
170        )
171        if launch.eval_batch_size is not None
172        else None
173    )
174 
175    return GrugRunConfig(
176        model=launch.model,
177        data=launch.data,
178        resources=launch.resources,
179        output_path=output_path,
180        optimizer=launch.optimizer,
181        trainer=grug_trainer,
182        eval=eval_config,
183    )
184 
185 
186def run_grug_base_trial(config: GrugBaseLaunchConfig) -> None:
187    """Build the full grug run config and dispatch the training job to Fray.
188 
189    Runs inline on the launcher; ``run_grug`` submits the job and blocks until it
190    completes.
191    """
192    run_grug(build_grug_run_config(config))
193 
194 
195def grug_base_trial(*, version: str | None = None) -> ArtifactStep[LevanterCheckpoint]:
196    """The base grug trial on the Nemotron mix as a lazy checkpoint.
197 
198    Every component is a :class:`Dataset` handle, so the whole graph lowers via
199    :func:`~marin.execution.lazy.lower`. The paloma/uncheatable suites are validation
200    (weight 0).
201    """
202    name = "grug/base-trial"
203    version = resolve_version(name, version)
204    nem = nemotron_datasets(tokenizer=llama3_tokenizer)
205    train = {nem[split]: weight for split, weight in _NEMOTRON_WEIGHTS.items()}
206    train[starcoder_dataset(tokenizer=llama3_tokenizer)] = _STARCODER_WEIGHT
207    train[proofpile_dataset(tokenizer=llama3_tokenizer)] = _PROOFPILE_WEIGHT
208    validation = [
209        *paloma_datasets(tokenizer=llama3_tokenizer).values(),
210        *uncheatable_datasets(tokenizer=llama3_tokenizer).values(),
211    ]
212 
213    run_id = _resolve_run_id("grug-base-trial")
214 
215    def build_config(ctx: StepContext) -> GrugBaseLaunchConfig:
216        return GrugBaseLaunchConfig(
217            model=GRUG_130M_MODEL,
218            data=mixture(ctx, train, validation=validation),
219            output_path=ctx.output_path,
220            run_id=run_id,
221            resources=ctx.runtime_arg("train_resources"),
222            steps=2_000,
223            batch_size=512,
224            seed=0,
225            mp="params=float32,compute=bfloat16,output=bfloat16",
226            tracker=WandbConfig(
227                project="marin",
228                tags=["grug", "template"],
229                group="grug-base-trial",
230                name=None,  # filled from run_id in _resolve_tracker
231                replicate_path=ctx.output_path,
232            ),
233            optimizer=AdamConfig(
234                learning_rate=3e-3,
235                weight_decay=0.1,
236                lr_schedule="cosine",
237                decay=0.2,
238                min_lr_ratio=0.1,
239                warmup=1000,
240            ),
241            z_loss_weight=1e-4,
242            ema_beta=None,
243            log_every=1,
244            eval_batch_size=512,
245            steps_per_eval=1000,
246            max_eval_batches=8,
247            eval_current=True,
248            eval_ema=False,
249        )
250 
251    return ArtifactStep(
252        name=user_namespaced_name(name, version),
253        version=version,
254        artifact_type=LevanterCheckpoint,
255        run=run_grug_base_trial,
256        build_config=build_config,
257        deps=(*train, *validation),
258        runtime_args={"train_resources": _TRAIN_RESOURCES},
259    )
260 
261 
262if __name__ == "__main__":
263    experiment_main(grug_base_trial)()

Back to top