kieve-moe 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: kieve-moe
3
+ Version: 0.1.0
4
+ Summary: Expert-parallel MoE training over iroh-tinker: fractal tiling + procedural data + discrete diffusion
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: numpy>=1.24
9
+ Requires-Dist: torch>=2.0
10
+ Requires-Dist: iroh-tinker>=0.5.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: ruff>=0.5; extra == "dev"
14
+ Requires-Dist: mypy>=1.10; extra == "dev"
15
+
16
+ # kieve-moe
17
+
18
+ Expert-parallel **Mixture-of-Experts** training over
19
+ [iroh-tinker](https://github.com/scairesearch/iroh_tinker), built for the
20
+ GCP fleet (CPU/GPU mix) and integrating with
21
+ [elasticgrid](https://pypi.org/project/elasticgrid/) for launch/placement.
22
+
23
+ `kieve-moe` scales a BertBM/Kieve-style MoE encoder: experts are sharded
24
+ across ranks, tokens are routed to the rank that owns their selected experts
25
+ over an iroh-tinker transport, and gradients are averaged with the ring
26
+ allreduce on the same transport — no NCCL/gloo process group required.
27
+
28
+ ## Features
29
+
30
+ - **Expert-parallel MoE** (`expert_parallel.py`): top-k GeGLU MoE with a
31
+ load-balancing auxiliary loss; remote experts are served by an aggregator
32
+ rank over a dedicated transport (requests and replies ride disjoint
33
+ channels).
34
+ - **Distributed trainer** (`distributed.py`): data-parallel gradient sync,
35
+ barriers, per-rank procedural data shards, resume-from-step checkpointing.
36
+ - **Fractal tiling routing** (`fractal.py`): self-similar hierarchical
37
+ routing (branch^levels expert tree) mirroring the Taichi fractal-tiling
38
+ playground — a cheap router for very large expert pools.
39
+ - **Procedural generation** (`procedural.py`): seeded pattern/LFSR token
40
+ streams, so the fleet trains without shipping a dataset.
41
+ - **Discrete diffusion head** (`diffusion.py`): masked-absorbing-state (MDLM)
42
+ denoising objective + iterative unmasking sampler, adding generative
43
+ token modeling on top of the MoE encoder.
44
+ - **Parametric sizing** (`scaling.py`): the flagship **4.5B** configuration
45
+ (15 layers, H=768, I=4096, E=32) with a live parameter estimate.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install kieve-moe
51
+ pip install -e ".[dev]" # from a checkout
52
+ ```
53
+
54
+ ## Train (local, in-process simulated world)
55
+
56
+ ```bash
57
+ kieve-moe train --config configs/moe-4-5b.json --world 2 --steps 20
58
+ ```
59
+
60
+ ## Train on the GCP fleet
61
+
62
+ Use iroh-tinker's `SocketTransport` (`--transport socket`) with a shared
63
+ rendezvous dir, and elasticgrid for placement/launch:
64
+
65
+ ```bash
66
+ kieve-moe train --config configs/moe-4-5b.json --world 4 \
67
+ --transport socket --rdv /tmp/rdv --host 0.0.0.0
68
+ ```
69
+
70
+ Or build an elasticgrid `LaunchSpec` for the same config (see
71
+ `scripts/launch_elasticgrid.py`).
72
+
73
+ ## Tests
74
+
75
+ `pytest tests -q` — 15 tests covering dense↔parallel MoE equivalence,
76
+ procedural streams, fractal routing, diffusion sampling, scale estimates, and
77
+ the distributed trainer loop.
@@ -0,0 +1,62 @@
1
+ # kieve-moe
2
+
3
+ Expert-parallel **Mixture-of-Experts** training over
4
+ [iroh-tinker](https://github.com/scairesearch/iroh_tinker), built for the
5
+ GCP fleet (CPU/GPU mix) and integrating with
6
+ [elasticgrid](https://pypi.org/project/elasticgrid/) for launch/placement.
7
+
8
+ `kieve-moe` scales a BertBM/Kieve-style MoE encoder: experts are sharded
9
+ across ranks, tokens are routed to the rank that owns their selected experts
10
+ over an iroh-tinker transport, and gradients are averaged with the ring
11
+ allreduce on the same transport — no NCCL/gloo process group required.
12
+
13
+ ## Features
14
+
15
+ - **Expert-parallel MoE** (`expert_parallel.py`): top-k GeGLU MoE with a
16
+ load-balancing auxiliary loss; remote experts are served by an aggregator
17
+ rank over a dedicated transport (requests and replies ride disjoint
18
+ channels).
19
+ - **Distributed trainer** (`distributed.py`): data-parallel gradient sync,
20
+ barriers, per-rank procedural data shards, resume-from-step checkpointing.
21
+ - **Fractal tiling routing** (`fractal.py`): self-similar hierarchical
22
+ routing (branch^levels expert tree) mirroring the Taichi fractal-tiling
23
+ playground — a cheap router for very large expert pools.
24
+ - **Procedural generation** (`procedural.py`): seeded pattern/LFSR token
25
+ streams, so the fleet trains without shipping a dataset.
26
+ - **Discrete diffusion head** (`diffusion.py`): masked-absorbing-state (MDLM)
27
+ denoising objective + iterative unmasking sampler, adding generative
28
+ token modeling on top of the MoE encoder.
29
+ - **Parametric sizing** (`scaling.py`): the flagship **4.5B** configuration
30
+ (15 layers, H=768, I=4096, E=32) with a live parameter estimate.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install kieve-moe
36
+ pip install -e ".[dev]" # from a checkout
37
+ ```
38
+
39
+ ## Train (local, in-process simulated world)
40
+
41
+ ```bash
42
+ kieve-moe train --config configs/moe-4-5b.json --world 2 --steps 20
43
+ ```
44
+
45
+ ## Train on the GCP fleet
46
+
47
+ Use iroh-tinker's `SocketTransport` (`--transport socket`) with a shared
48
+ rendezvous dir, and elasticgrid for placement/launch:
49
+
50
+ ```bash
51
+ kieve-moe train --config configs/moe-4-5b.json --world 4 \
52
+ --transport socket --rdv /tmp/rdv --host 0.0.0.0
53
+ ```
54
+
55
+ Or build an elasticgrid `LaunchSpec` for the same config (see
56
+ `scripts/launch_elasticgrid.py`).
57
+
58
+ ## Tests
59
+
60
+ `pytest tests -q` — 15 tests covering dense↔parallel MoE equivalence,
61
+ procedural streams, fractal routing, diffusion sampling, scale estimates, and
62
+ the distributed trainer loop.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "kieve-moe"
7
+ version = "0.1.0"
8
+ description = "Expert-parallel MoE training over iroh-tinker: fractal tiling + procedural data + discrete diffusion"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ dependencies = [
13
+ "numpy>=1.24",
14
+ "torch>=2.0",
15
+ "iroh-tinker>=0.5.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.0",
21
+ "ruff>=0.5",
22
+ "mypy>=1.10",
23
+ ]
24
+
25
+ [project.scripts]
26
+ kieve-moe = "kieve_moe.cli:main"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ target-version = "py310"
34
+
35
+ [tool.ruff.lint]
36
+ extend-select = ["E", "F", "I", "UP", "B", "SIM", "T20"]
37
+
38
+ [tool.ruff.lint.per-file-ignores]
39
+ "tests/*" = ["T20"]
40
+
41
+ [tool.mypy]
42
+ python_version = "3.10"
43
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ """kieve-moe: expert-parallel MoE training over iroh-tinker.
2
+
3
+ This is the scaling layer for BertBM-style Mixture-of-Experts models: the
4
+ dense GeGLU MoE block (BertBM ``moe.py``) is split across ranks so each
5
+ process owns a shard of the experts, a router sends tokens to the rank that
6
+ owns their selected experts, and activations flow back over the
7
+ ``iroh_tinker.comm.Transport`` interface (in-process ``LocalTransport`` for
8
+ tests, ``SocketTransport``/native transports for real hosts). Gradients are
9
+ synchronized with the ring allreduce on the same transports, so the whole
10
+ training loop is decentralized — no NCCL/gloo process group required.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = ["__version__"]
@@ -0,0 +1,100 @@
1
+ """``kieve-moe`` command line interface.
2
+
3
+ Subcommands:
4
+
5
+ ``train`` run the distributed MoE training loop
6
+ ``encode`` not yet implemented (reserved)
7
+
8
+ Every command supports ``--json``.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from collections.abc import Sequence
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from .config import MoEConfig
20
+ from .distributed import DistributedTrainer, TrainConfig
21
+
22
+ __all__ = ["build_parser", "main"]
23
+
24
+
25
+ def _load_config(path: str) -> MoEConfig:
26
+ raw = json.loads(Path(path).read_text())
27
+ known = {f for f in MoEConfig.__dataclass_fields__ if f != "extra"} # type: ignore[attr-defined]
28
+ return MoEConfig(**{k: v for k, v in raw.items() if k in known})
29
+
30
+
31
+ def build_parser() -> argparse.ArgumentParser:
32
+ parser = argparse.ArgumentParser(
33
+ prog="kieve-moe",
34
+ description="Expert-parallel MoE training over iroh-tinker.",
35
+ )
36
+ parser.add_argument("--json", action="store_true", help="emit JSON on stdout")
37
+ sub = parser.add_subparsers(dest="command", required=True)
38
+
39
+ train = sub.add_parser("train", help="run the distributed MoE training loop")
40
+ train.add_argument("--config", required=True, help="config JSON file")
41
+ train.add_argument("--world", type=int, default=1, help="world size")
42
+ train.add_argument("--rank", type=int, default=0, help="this rank")
43
+ train.add_argument("--steps", type=int, default=20, help="number of steps")
44
+ train.add_argument("--lr", type=float, default=3e-4, help="learning rate")
45
+ train.add_argument(
46
+ "--transport", default="local", choices=["local", "socket"], help="transport"
47
+ )
48
+ train.add_argument("--rdv", default=".cache/iroh_tinker", help="rendezvous dir (socket)")
49
+ train.add_argument("--host", default="127.0.0.1", help="bind host (socket)")
50
+ train.add_argument("--json", action="store_true", help="emit JSON on stdout")
51
+ return parser
52
+
53
+
54
+ def run_train(args: argparse.Namespace) -> int:
55
+ cfg = _load_config(args.config)
56
+ cfg.world_size = args.world
57
+ train_cfg = TrainConfig(steps=args.steps, lr=args.lr)
58
+
59
+ if args.world == 1 or args.transport == "local":
60
+ from iroh_tinker.comm import LocalNetwork
61
+
62
+ net = LocalNetwork(args.world)
63
+ transport: Any = net.transport(args.rank)
64
+ else:
65
+ from iroh_tinker.comm import SocketTransport
66
+
67
+ transport = SocketTransport(
68
+ rank=args.rank, world_size=args.world, rendezvous_dir=args.rdv, host=args.host
69
+ )
70
+
71
+ try:
72
+ trainer = DistributedTrainer(args.rank, args.world, transport, cfg, train_cfg)
73
+ out = trainer.run()
74
+ finally:
75
+ transport.close()
76
+ if args.json:
77
+ sys.stdout.write(json.dumps(out, indent=2) + "\n")
78
+ else:
79
+ pass
80
+ return 0
81
+
82
+
83
+ def main(argv: Sequence[str] | None = None) -> int:
84
+ parser = build_parser()
85
+ args = parser.parse_args(list(argv) if argv is not None else None)
86
+ try:
87
+ if args.command == "train":
88
+ return run_train(args)
89
+ except (FileNotFoundError, ValueError, RuntimeError) as exc:
90
+ if getattr(args, "json", False):
91
+ sys.stdout.write(json.dumps({"error": str(exc)}) + "\n")
92
+ else:
93
+ pass
94
+ return 2
95
+ parser.error(f"unknown command {args.command!r}")
96
+ return 2
97
+
98
+
99
+ if __name__ == "__main__": # pragma: no cover
100
+ raise SystemExit(main())
@@ -0,0 +1,90 @@
1
+ """Model configuration for kieve-moe.
2
+
3
+ The dataclass mirrors the BertBM/ModernBERT MoE fields (``moe_experts``,
4
+ ``moe_top_k``, ``moe_aux_loss``) so a config written for BertBM can be reused
5
+ verbatim; a local ``MoEConfig`` keeps the package self-contained when the
6
+ ``bertbm`` package is not installed.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import asdict, dataclass, field
11
+ from typing import Any
12
+
13
+ __all__ = ["MoEConfig", "model_from_config", "as_dict"]
14
+
15
+
16
+ @dataclass
17
+ class MoEConfig:
18
+ # architecture
19
+ vocab_size: int = 256
20
+ hidden_size: int = 64
21
+ intermediate_size: int = 128
22
+ num_layers: int = 2
23
+ num_experts: int = 8
24
+ top_k: int = 2
25
+ aux_loss_coef: float = 0.01
26
+ max_seq_len: int = 64
27
+ # training
28
+ lr: float = 3e-4
29
+ weight_decay: float = 0.0
30
+ grad_clip: float = 1.0
31
+ batch_size: int = 8
32
+ # architecture enhancements
33
+ fractal_levels: int = 0 # >0: fractal-tiled hierarchical routing (branch**levels experts)
34
+ fractal_branch: int = 2
35
+ diffusion: bool = False # add discrete (masked) diffusion denoising head
36
+ procedural: bool = True # generate training tokens procedurally (no dataset needed)
37
+ # expert placement: expert i is owned by rank (i % world_size)
38
+ world_size: int = 1
39
+ # hidden knobs
40
+ layernorm_eps: float = 1e-5
41
+ seed: int = 42
42
+ extra: dict[str, Any] = field(default_factory=dict)
43
+
44
+ def validate(self) -> None:
45
+ if self.num_experts < 1:
46
+ raise ValueError("num_experts must be >= 1")
47
+ if not 0 < self.top_k <= self.num_experts:
48
+ raise ValueError(f"top_k must be in 1..{self.num_experts}")
49
+ if self.hidden_size < 1 or self.intermediate_size < 1:
50
+ raise ValueError("hidden/intermediate sizes must be positive")
51
+ if self.num_layers < 1:
52
+ raise ValueError("num_layers must be >= 1")
53
+ if self.world_size < 1:
54
+ raise ValueError("world_size must be >= 1")
55
+ if self.aux_loss_coef < 0:
56
+ raise ValueError("aux_loss_coef must be non-negative")
57
+ if self.fractal_levels > 0:
58
+ if self.fractal_levels > 0 and self.fractal_branch < 2:
59
+ raise ValueError("fractal_branch must be >= 2")
60
+ total = self.fractal_branch ** self.fractal_levels
61
+ if self.num_experts != total:
62
+ raise ValueError(
63
+ f"fractal tiling requires num_experts == branch**levels "
64
+ f"({self.fractal_branch}**{self.fractal_levels}={total})"
65
+ )
66
+
67
+ @classmethod
68
+ def from_bertbm(cls, cfg: Any, **overrides: Any) -> MoEConfig:
69
+ """Adopt a BertBM ModernBERTConfig (or any object with the fields)."""
70
+ return cls(
71
+ vocab_size=int(getattr(cfg, "vocab_size", 256)),
72
+ hidden_size=int(getattr(cfg, "hidden_size", 64)),
73
+ intermediate_size=int(getattr(cfg, "intermediate_size", 128)),
74
+ num_layers=int(getattr(cfg, "num_hidden_layers", 2)),
75
+ num_experts=int(getattr(cfg, "moe_experts", 8)),
76
+ top_k=int(getattr(cfg, "moe_top_k", 2)),
77
+ aux_loss_coef=float(getattr(cfg, "moe_aux_loss", 0.01)),
78
+ **overrides,
79
+ )
80
+
81
+
82
+ def model_from_config(cfg: MoEConfig, transport: Any | None = None, rank: int = 0):
83
+ """Build the MoE encoder (licensed import to avoid the torch import here)."""
84
+ from .model import MoEEncoder
85
+
86
+ return MoEEncoder(cfg, transport=transport, rank=rank)
87
+
88
+
89
+ def as_dict(cfg: MoEConfig) -> dict[str, Any]:
90
+ return asdict(cfg)
@@ -0,0 +1,73 @@
1
+ """Discrete diffusion auxiliary head for generative token modeling.
2
+
3
+ Implements an absorbing-state (masked) discrete diffusion over the token
4
+ vocabulary — a lightweight MDLM. During training the head is offered a batch
5
+ of sequences, a random timestep is sampled, a fraction of tokens are masked and
6
+ the head predicts the original tokens from the masked sequence (a denoising/
7
+ unmasking objective added to the MoE cross-entropy). Sampling iteratively
8
+ unmasks from a seed, giving a procedural generative extension on top of the
9
+ MoE encoder.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+
17
+ __all__ = ["DiscreteDiffusionHead"]
18
+
19
+
20
+ class DiscreteDiffusionHead(nn.Module):
21
+ def __init__(self, vocab_size: int, hidden_size: int, mask_token: int | None = None):
22
+ super().__init__()
23
+ self.vocab_size = vocab_size
24
+ self.mask_token = vocab_size if mask_token is None else mask_token
25
+ self.classifier = nn.Linear(hidden_size, vocab_size + 1, bias=False)
26
+
27
+ @property
28
+ def mask_id(self) -> int:
29
+ return self.mask_token
30
+
31
+ def loss(self, hidden: torch.Tensor, ids: torch.Tensor) -> torch.Tensor:
32
+ """Masked-diffusion denoising loss over a batch."""
33
+ b, t, d = hidden.shape
34
+ rng = torch.ones((b, t), device=hidden.device).bernoulli_(0.15)
35
+ masked = ids.clone()
36
+ masked[rng.bool()] = self.mask_token
37
+ logits = self.classifier(hidden) # [B,T,V+1]
38
+ target = ids[rng.bool()] if rng.any() else ids
39
+ if rng.any():
40
+ loss = F.cross_entropy(logits[rng.bool()], target)
41
+ else:
42
+ loss = torch.zeros((), device=hidden.device)
43
+ return loss
44
+
45
+ def sample(
46
+ self,
47
+ hidden_fn,
48
+ seed: torch.Tensor,
49
+ steps: int,
50
+ temperature: float = 1.0,
51
+ ) -> torch.Tensor:
52
+ """Iteratively unmask seed tokens using the encoder hidden states."""
53
+ ids = seed.clone()
54
+ schedule = torch.linspace(0.15, 0.0, steps)
55
+ with torch.no_grad():
56
+ for frac in schedule:
57
+ hidden = hidden_fn(ids)
58
+ logits = self.classifier(hidden)
59
+ probs = F.softmax(logits / temperature, dim=-1)
60
+ pred = probs.argmax(-1)
61
+ mask = (ids == self.mask_token)
62
+ if not bool(mask.any()):
63
+ break
64
+ # progressively unmask a fraction
65
+ n_unmask = int(mask.sum() * float(frac))
66
+ candidates = mask.nonzero(as_tuple=False)
67
+ if n_unmask > 0 and candidates.numel() > 0:
68
+ pick = candidates[:n_unmask]
69
+ ids[pick[:, 0], pick[:, 1]] = pred[pick[:, 0], pick[:, 1]]
70
+ else:
71
+ ids[mask] = pred[mask]
72
+ ids[ids == self.mask_token] = pred[ids == self.mask_token]
73
+ return ids
@@ -0,0 +1,139 @@
1
+ """Distributed training loop for kieve-moe.
2
+
3
+ Data-parallel gradient synchronization over iroh-tinker transports plus the
4
+ expert-parallel MoE layers. The loop is deliberately small and transport-
5
+ agnostic: any ``Transport`` implementation (in-process ``LocalTransport``,
6
+ TCP ``SocketTransport``, native mesh) can be used, and gradients are averaged
7
+ with the ring allreduce from ``iroh_tinker.comm``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+ import torch
17
+
18
+ from .config import MoEConfig
19
+ from .model import MoEEncoder
20
+
21
+ __all__ = ["TrainConfig", "DistributedTrainer"]
22
+
23
+
24
+ @dataclass
25
+ class TrainConfig:
26
+ steps: int = 20
27
+ lr: float = 3e-4
28
+ log_every: int = 5
29
+ seed: int = 42
30
+ checkpoint_dir: str = ".cache/kieve-moe"
31
+ resume: bool = True
32
+
33
+
34
+ class DistributedTrainer:
35
+ """Owner of a rank's model, transport and optimizer loop."""
36
+
37
+ def __init__(
38
+ self,
39
+ rank: int,
40
+ world_size: int,
41
+ transport: Any,
42
+ cfg: MoEConfig,
43
+ train_cfg: TrainConfig | None = None,
44
+ moe_network: Any | None = None,
45
+ ):
46
+ self.rank = rank
47
+ self.world_size = world_size
48
+ self.transport = transport
49
+ self.cfg = cfg
50
+ cfg.world_size = world_size
51
+ self.train_cfg = train_cfg or TrainConfig()
52
+ torch.manual_seed(cfg.seed + rank)
53
+ # Dedicated MoE dispatch transport so the expert RPC never collides
54
+ # with the gradient collective frames on the shared transport.
55
+ self._moe_transport: Any | None = None
56
+ if world_size > 1:
57
+ from iroh_tinker.comm import LocalNetwork
58
+
59
+ if moe_network is None:
60
+ moe_network = LocalNetwork(world_size)
61
+ self._moe_net = moe_network
62
+ self._moe_transport = self._moe_net.transport(rank)
63
+ self.model = MoEEncoder(
64
+ cfg,
65
+ transport=transport,
66
+ rank=rank,
67
+ moe_transport=self._moe_transport,
68
+ )
69
+ self._param_map = list(self.model.parameters())
70
+
71
+ def _step_data(self, step: int) -> tuple[torch.Tensor, torch.Tensor]:
72
+ """Local data shard: a deterministic stream unique per rank."""
73
+ if self.cfg.procedural:
74
+ from .procedural import make_stream
75
+
76
+ stream = make_stream(self.cfg, rank=self.rank, world_size=self.world_size)
77
+ return stream.batch()
78
+ g = torch.Generator().manual_seed(
79
+ self.train_cfg.seed + step * 1000 + self.rank
80
+ )
81
+ ids = torch.randint(
82
+ 0, self.cfg.vocab_size, (self.cfg.batch_size, self.cfg.max_seq_len), generator=g
83
+ )
84
+ targets = torch.randint(
85
+ 0, self.cfg.vocab_size, (self.cfg.batch_size, self.cfg.max_seq_len), generator=g
86
+ )
87
+ return ids, targets
88
+
89
+ def _allreduce_grads(self) -> None:
90
+ """Average every parameter gradient across ranks (ring allreduce)."""
91
+ from iroh_tinker.comm import RingAllreduce
92
+
93
+ if self.world_size <= 1:
94
+ return
95
+ flat: dict[str, np.ndarray] = {}
96
+ shapes: dict[str, tuple[int, ...]] = {}
97
+ for i, p in enumerate(self._param_map):
98
+ if p.grad is None:
99
+ continue
100
+ key = f"p{i}"
101
+ shapes[key] = tuple(p.grad.shape)
102
+ flat[key] = p.grad.detach().cpu().numpy().reshape(-1).astype(np.float32)
103
+ reduced = RingAllreduce(self.transport).allreduce(flat)
104
+ for key, arr in reduced.items():
105
+ self._param_map[int(key[1:])].grad = (
106
+ torch.from_numpy(arr).reshape(shapes[key]).to(self._param_map[int(key[1:])].device)
107
+ )
108
+
109
+ def run(self) -> dict[str, Any]:
110
+ t0 = time.perf_counter()
111
+ start_step = 0
112
+ if self.train_cfg.resume and self.rank == 0:
113
+ import json
114
+ from pathlib import Path
115
+
116
+ state_path = Path(self.train_cfg.checkpoint_dir) / "state.json"
117
+ if state_path.exists():
118
+ try:
119
+ start_step = int(json.loads(state_path.read_text()).get("step", 0))
120
+ except Exception:
121
+ start_step = 0
122
+
123
+ for step in range(start_step, self.train_cfg.steps):
124
+ ids, targets = self._step_data(step)
125
+ self.model.step(ids, targets, self.train_cfg.lr)
126
+ self._allreduce_grads()
127
+ self.transport.barrier()
128
+ if step % self.train_cfg.log_every == 0 and self.rank == 0:
129
+ pass
130
+
131
+ self.transport.barrier()
132
+ elapsed = time.perf_counter() - t0
133
+ return {
134
+ "rank": self.rank,
135
+ "world_size": self.world_size,
136
+ "steps": self.train_cfg.steps,
137
+ "elapsed_s": round(elapsed, 3),
138
+ "status": "ok",
139
+ }