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.
- kieve_moe-0.1.0/PKG-INFO +77 -0
- kieve_moe-0.1.0/README.md +62 -0
- kieve_moe-0.1.0/pyproject.toml +43 -0
- kieve_moe-0.1.0/setup.cfg +4 -0
- kieve_moe-0.1.0/src/kieve_moe/__init__.py +16 -0
- kieve_moe-0.1.0/src/kieve_moe/cli.py +100 -0
- kieve_moe-0.1.0/src/kieve_moe/config.py +90 -0
- kieve_moe-0.1.0/src/kieve_moe/diffusion.py +73 -0
- kieve_moe-0.1.0/src/kieve_moe/distributed.py +139 -0
- kieve_moe-0.1.0/src/kieve_moe/expert_parallel.py +261 -0
- kieve_moe-0.1.0/src/kieve_moe/fractal.py +85 -0
- kieve_moe-0.1.0/src/kieve_moe/model.py +147 -0
- kieve_moe-0.1.0/src/kieve_moe/procedural.py +61 -0
- kieve_moe-0.1.0/src/kieve_moe/scaling.py +73 -0
- kieve_moe-0.1.0/src/kieve_moe.egg-info/PKG-INFO +77 -0
- kieve_moe-0.1.0/src/kieve_moe.egg-info/SOURCES.txt +21 -0
- kieve_moe-0.1.0/src/kieve_moe.egg-info/dependency_links.txt +1 -0
- kieve_moe-0.1.0/src/kieve_moe.egg-info/entry_points.txt +2 -0
- kieve_moe-0.1.0/src/kieve_moe.egg-info/requires.txt +8 -0
- kieve_moe-0.1.0/src/kieve_moe.egg-info/top_level.txt +1 -0
- kieve_moe-0.1.0/tests/test_distributed.py +98 -0
- kieve_moe-0.1.0/tests/test_enhancements.py +88 -0
- kieve_moe-0.1.0/tests/test_expert_parallel.py +128 -0
kieve_moe-0.1.0/PKG-INFO
ADDED
|
@@ -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,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
|
+
}
|