ms-moe-maker 0.4.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.
- ms_moe_maker-0.4.0/MsMoEMaker.pyproj +54 -0
- ms_moe_maker-0.4.0/PKG-INFO +175 -0
- ms_moe_maker-0.4.0/README.md +159 -0
- ms_moe_maker-0.4.0/ms_moe_maker/__init__.py +28 -0
- ms_moe_maker-0.4.0/ms_moe_maker/__main__.py +253 -0
- ms_moe_maker-0.4.0/ms_moe_maker/_describe.py +47 -0
- ms_moe_maker-0.4.0/ms_moe_maker/_version.py +24 -0
- ms_moe_maker-0.4.0/ms_moe_maker/events.py +70 -0
- ms_moe_maker-0.4.0/ms_moe_maker/levers.py +285 -0
- ms_moe_maker-0.4.0/ms_moe_maker/manifest.py +285 -0
- ms_moe_maker-0.4.0/ms_moe_maker/recipe.py +573 -0
- ms_moe_maker-0.4.0/ms_moe_maker/runner.py +448 -0
- ms_moe_maker-0.4.0/ms_moe_maker/stages.py +123 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/PKG-INFO +175 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/SOURCES.txt +24 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/dependency_links.txt +1 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/entry_points.txt +2 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/requires.txt +4 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/scm_file_list.json +19 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/scm_version.json +8 -0
- ms_moe_maker-0.4.0/ms_moe_maker.egg-info/top_level.txt +1 -0
- ms_moe_maker-0.4.0/pyproject.toml +55 -0
- ms_moe_maker-0.4.0/recipe.example.yaml +121 -0
- ms_moe_maker-0.4.0/setup.cfg +4 -0
- ms_moe_maker-0.4.0/tests/test_cli.py +233 -0
- ms_moe_maker-0.4.0/tests/test_runner.py +301 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
5
|
+
<SchemaVersion>2.0</SchemaVersion>
|
|
6
|
+
<ProjectGuid>7c1f4a92-3e6d-4b18-9a4c-2f8b6d5e1a03</ProjectGuid>
|
|
7
|
+
<ProjectHome>.</ProjectHome>
|
|
8
|
+
<!-- F5 runs the CLI. Pass a recipe in the project's debug arguments. -->
|
|
9
|
+
<StartupFile>ms_moe_maker\__main__.py</StartupFile>
|
|
10
|
+
<SearchPath>.</SearchPath>
|
|
11
|
+
<WorkingDirectory>.</WorkingDirectory>
|
|
12
|
+
<OutputPath>.</OutputPath>
|
|
13
|
+
<Name>MsMoEMaker</Name>
|
|
14
|
+
<RootNamespace>ms_moe_maker</RootNamespace>
|
|
15
|
+
<InterpreterId>Global|PythonCore|3.11</InterpreterId>
|
|
16
|
+
<IsWindowsApplication>False</IsWindowsApplication>
|
|
17
|
+
<Description>Build a mixture of experts from deliberately chosen specialists.</Description>
|
|
18
|
+
<TestFramework>Pytest</TestFramework>
|
|
19
|
+
</PropertyGroup>
|
|
20
|
+
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
21
|
+
<DebugSymbols>true</DebugSymbols>
|
|
22
|
+
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
|
23
|
+
</PropertyGroup>
|
|
24
|
+
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
|
25
|
+
<DebugSymbols>true</DebugSymbols>
|
|
26
|
+
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
|
27
|
+
</PropertyGroup>
|
|
28
|
+
<ItemGroup>
|
|
29
|
+
<Compile Include="ms_moe_maker\__init__.py" />
|
|
30
|
+
<Compile Include="ms_moe_maker\__main__.py" />
|
|
31
|
+
<Compile Include="ms_moe_maker\_describe.py" />
|
|
32
|
+
<Compile Include="ms_moe_maker\events.py" />
|
|
33
|
+
<Compile Include="ms_moe_maker\levers.py" />
|
|
34
|
+
<Compile Include="ms_moe_maker\manifest.py" />
|
|
35
|
+
<Compile Include="ms_moe_maker\recipe.py" />
|
|
36
|
+
<Compile Include="ms_moe_maker\runner.py" />
|
|
37
|
+
<Compile Include="ms_moe_maker\stages.py" />
|
|
38
|
+
<Compile Include="tests\test_cli.py" />
|
|
39
|
+
<Compile Include="tests\test_runner.py" />
|
|
40
|
+
</ItemGroup>
|
|
41
|
+
<ItemGroup>
|
|
42
|
+
<Folder Include="ms_moe_maker\" />
|
|
43
|
+
<Folder Include="tests\" />
|
|
44
|
+
</ItemGroup>
|
|
45
|
+
<ItemGroup>
|
|
46
|
+
<Content Include="pyproject.toml" />
|
|
47
|
+
<Content Include="README.md" />
|
|
48
|
+
<Content Include="recipe.example.yaml" />
|
|
49
|
+
</ItemGroup>
|
|
50
|
+
<ItemGroup>
|
|
51
|
+
<InterpreterReference Include="Global|PythonCore|3.11" />
|
|
52
|
+
</ItemGroup>
|
|
53
|
+
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
|
|
54
|
+
</Project>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ms-moe-maker
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Build a mixture of experts from deliberately chosen specialists. Not a coding model - a coding model shaped like your stack.
|
|
5
|
+
Author: Chad Roesler
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/ChadRoesler/MsMoEMaker
|
|
8
|
+
Project-URL: Repository, https://github.com/ChadRoesler/MsMoEMaker
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/ChadRoesler/MsMoEMaker/issues
|
|
10
|
+
Keywords: moe,mixture-of-experts,qwen,fine-tuning,gguf,llm
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: pyyaml>=6.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# Ms.MoE
|
|
18
|
+
|
|
19
|
+
**Multi-Specified Mixture of Experts.** Five deliberate experts instead of a
|
|
20
|
+
hundred lottery tickets.
|
|
21
|
+
|
|
22
|
+
The design thesis is the inverse of a frontier MoE. Instead of training many
|
|
23
|
+
experts and hoping specialisation emerges — then fighting dead and collapsed
|
|
24
|
+
experts with a load-balancing auxiliary loss — you *hand-assign* the domains.
|
|
25
|
+
Every expert has a guaranteed constituency, so none of them can go dead,
|
|
26
|
+
because none of them was speculative.
|
|
27
|
+
|
|
28
|
+
The corollary is what makes it maintainable by one person: because each expert
|
|
29
|
+
does exactly one thing, you can retrain **one** and re-splice without touching
|
|
30
|
+
the others.
|
|
31
|
+
|
|
32
|
+
> Not a coding model. A coding model shaped like *your* stack.
|
|
33
|
+
|
|
34
|
+
The real product is the factory, not the model. Swap the expert list and
|
|
35
|
+
someone else gets their own Ms.MoE.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install ms-moe-maker
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That gets you the CLI and the contract — about a megabyte, no torch. The heavy
|
|
46
|
+
machinery lives in the pipeline this forks, in whatever venv you train in. That
|
|
47
|
+
split is the point: `ms-moe-maker validate` runs on a laptop, so you can check a
|
|
48
|
+
recipe and see what it will cost before going near a machine that can run it.
|
|
49
|
+
|
|
50
|
+
## Use
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ms-moe-maker describe # one line of JSON, exit 0, no side effects
|
|
54
|
+
ms-moe-maker validate recipe.yaml # parse, check, translate — touches nothing
|
|
55
|
+
ms-moe-maker build recipe.yaml # run it
|
|
56
|
+
ms-moe-maker build recipe.yaml --json # JSON Lines on stdout, prose on stderr
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`ms-moe-maker build recipe.yaml` is the literal command. It's what's in this README,
|
|
60
|
+
it's what a person types, and it's exactly what `seren-theatre[stagehand]`
|
|
61
|
+
forks — no separate API path with different defaults. If those two ever
|
|
62
|
+
diverged, the hand-run path would rot, because it's the one with no automated
|
|
63
|
+
users. Making them identical removes the possibility.
|
|
64
|
+
|
|
65
|
+
## The recipe
|
|
66
|
+
|
|
67
|
+
A build, as a document. The point is that you can hand it to someone who
|
|
68
|
+
doesn't have your box and they get your run — that's the difference between
|
|
69
|
+
"it works, look" and a result.
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
schema_version: 1
|
|
73
|
+
name: msmoe-coder-5x-dryrun
|
|
74
|
+
size: 0.5B
|
|
75
|
+
base: huihui-ai/Qwen2.5-Coder-0.5B-Instruct-abliterated
|
|
76
|
+
|
|
77
|
+
experts:
|
|
78
|
+
- name: powershell
|
|
79
|
+
source: { kind: hf, repo: SaeedRahmani/codeparrot_github_code_powershell, text_field: code }
|
|
80
|
+
- name: python
|
|
81
|
+
source: { kind: stack, language: Python }
|
|
82
|
+
# ...
|
|
83
|
+
|
|
84
|
+
budget:
|
|
85
|
+
target_steps: 150 # 1200 for a real rung; 150 is the shakedown
|
|
86
|
+
max_seq_length: 2048
|
|
87
|
+
per_device_batch: 4
|
|
88
|
+
grad_accum: 2
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See `recipe.example.yaml` for the annotated version — every field carries the
|
|
92
|
+
measurement that chose it.
|
|
93
|
+
|
|
94
|
+
Budgets are in **tokens**, derived from steps. Capping documents instead looked
|
|
95
|
+
like balance and wasn't: at 10,000 documents each, PowerShell received 4.3× the
|
|
96
|
+
gradient updates Shell did, and a different LR curve besides.
|
|
97
|
+
|
|
98
|
+
## Refusals — read this bit
|
|
99
|
+
|
|
100
|
+
Right now `ms-moe-maker build` drives an existing pipeline script by setting
|
|
101
|
+
environment variables and forking it. That script exposes sixteen levers. A
|
|
102
|
+
recipe declares far more than sixteen things.
|
|
103
|
+
|
|
104
|
+
So the naive wrapper would accept `per_device_batch: 8`, run the build at 4,
|
|
105
|
+
and report success — leaving you with a document that *looks* authoritative and
|
|
106
|
+
silently isn't. That's the worst possible place to put that trap, in the one
|
|
107
|
+
file whose entire selling point is reproducing someone else's run.
|
|
108
|
+
|
|
109
|
+
**So a recipe field is honoured, or the build refuses. Never ignored.**
|
|
110
|
+
|
|
111
|
+
The check isn't "is there a lever" — it's "will the run actually do what the
|
|
112
|
+
document says". Ms.MoE reads the pipeline's own constants statically (via
|
|
113
|
+
`ast`, never importing — importing it would cost you a CUDA context) and
|
|
114
|
+
compares each field against the value that will really be used. Agreement is
|
|
115
|
+
silence. Only disagreement refuses.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
$ ms-moe-maker build recipe.yaml
|
|
119
|
+
2 recipe field(s) cannot be honoured by fraunkenstein_universal.py:
|
|
120
|
+
· budget.per_device_batch=8 cannot be applied: the pipeline uses 4 from
|
|
121
|
+
PER_DEVICE_BATCH and exposes no environment lever for it.
|
|
122
|
+
· gates.main_evals='manual' cannot be honoured: the pipeline runs end to
|
|
123
|
+
end with no stage boundary a gate could pause at.
|
|
124
|
+
REFUSED - nothing was run.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--allow-refusals` proceeds anyway. The refusals are recorded in the run
|
|
128
|
+
manifest either way, because the person who needs to know a lever was ignored
|
|
129
|
+
is the one reading the dashboard six hours later, not the one who saw the
|
|
130
|
+
terminal at kickoff.
|
|
131
|
+
|
|
132
|
+
**The refusal list is the roadmap.** Each entry is a field somebody wanted to
|
|
133
|
+
set and couldn't — which is exactly the priority order for pulling that part of
|
|
134
|
+
the script into a real stage. When the list is empty, the decomposition is
|
|
135
|
+
finished, and nobody had to guess when.
|
|
136
|
+
|
|
137
|
+
## The run manifest
|
|
138
|
+
|
|
139
|
+
A build writes `msmoe-run.json` into its run directory: what the run is, the
|
|
140
|
+
ordered stage list, each stage's status and artifact, and any refusals.
|
|
141
|
+
|
|
142
|
+
That file is the **only** interface between this package and any viewer.
|
|
143
|
+
Nothing imports anything. `seren-theatre` reads the manifest when it's there
|
|
144
|
+
and falls back to reading the directory when it isn't — so an instrumented run
|
|
145
|
+
is exact and an uninstrumented directory still works. Neither package is
|
|
146
|
+
required, and neither knows the other exists.
|
|
147
|
+
|
|
148
|
+
## Events
|
|
149
|
+
|
|
150
|
+
Under `--json`: one JSON object per line on stdout, prose on stderr, never
|
|
151
|
+
interleaved.
|
|
152
|
+
|
|
153
|
+
| event | when |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `started` | the build begins; carries the resolved env and run dir |
|
|
156
|
+
| `stage` | a stage changes status |
|
|
157
|
+
| `progress` | something worth knowing inside a stage |
|
|
158
|
+
| `refused` | recipe fields that couldn't be honoured |
|
|
159
|
+
| `warning` / `error` | trouble |
|
|
160
|
+
| `done` | terminal, with `ok` |
|
|
161
|
+
|
|
162
|
+
Every line is flushed. A consumer following a six-hour build through a pipe
|
|
163
|
+
would otherwise see nothing until the buffer filled — and that looks exactly
|
|
164
|
+
like a hang.
|
|
165
|
+
|
|
166
|
+
## Status
|
|
167
|
+
|
|
168
|
+
The stage machinery, contract and CLI are real. The pipeline itself is still
|
|
169
|
+
the original 2483-line script, driven from the outside — **wrap-then-carve**.
|
|
170
|
+
The contract is the product; the internals move behind it without anything
|
|
171
|
+
downstream noticing.
|
|
172
|
+
|
|
173
|
+
## Licence
|
|
174
|
+
|
|
175
|
+
GPL-3.0-only.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Ms.MoE
|
|
2
|
+
|
|
3
|
+
**Multi-Specified Mixture of Experts.** Five deliberate experts instead of a
|
|
4
|
+
hundred lottery tickets.
|
|
5
|
+
|
|
6
|
+
The design thesis is the inverse of a frontier MoE. Instead of training many
|
|
7
|
+
experts and hoping specialisation emerges — then fighting dead and collapsed
|
|
8
|
+
experts with a load-balancing auxiliary loss — you *hand-assign* the domains.
|
|
9
|
+
Every expert has a guaranteed constituency, so none of them can go dead,
|
|
10
|
+
because none of them was speculative.
|
|
11
|
+
|
|
12
|
+
The corollary is what makes it maintainable by one person: because each expert
|
|
13
|
+
does exactly one thing, you can retrain **one** and re-splice without touching
|
|
14
|
+
the others.
|
|
15
|
+
|
|
16
|
+
> Not a coding model. A coding model shaped like *your* stack.
|
|
17
|
+
|
|
18
|
+
The real product is the factory, not the model. Swap the expert list and
|
|
19
|
+
someone else gets their own Ms.MoE.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install ms-moe-maker
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That gets you the CLI and the contract — about a megabyte, no torch. The heavy
|
|
30
|
+
machinery lives in the pipeline this forks, in whatever venv you train in. That
|
|
31
|
+
split is the point: `ms-moe-maker validate` runs on a laptop, so you can check a
|
|
32
|
+
recipe and see what it will cost before going near a machine that can run it.
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
ms-moe-maker describe # one line of JSON, exit 0, no side effects
|
|
38
|
+
ms-moe-maker validate recipe.yaml # parse, check, translate — touches nothing
|
|
39
|
+
ms-moe-maker build recipe.yaml # run it
|
|
40
|
+
ms-moe-maker build recipe.yaml --json # JSON Lines on stdout, prose on stderr
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`ms-moe-maker build recipe.yaml` is the literal command. It's what's in this README,
|
|
44
|
+
it's what a person types, and it's exactly what `seren-theatre[stagehand]`
|
|
45
|
+
forks — no separate API path with different defaults. If those two ever
|
|
46
|
+
diverged, the hand-run path would rot, because it's the one with no automated
|
|
47
|
+
users. Making them identical removes the possibility.
|
|
48
|
+
|
|
49
|
+
## The recipe
|
|
50
|
+
|
|
51
|
+
A build, as a document. The point is that you can hand it to someone who
|
|
52
|
+
doesn't have your box and they get your run — that's the difference between
|
|
53
|
+
"it works, look" and a result.
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
schema_version: 1
|
|
57
|
+
name: msmoe-coder-5x-dryrun
|
|
58
|
+
size: 0.5B
|
|
59
|
+
base: huihui-ai/Qwen2.5-Coder-0.5B-Instruct-abliterated
|
|
60
|
+
|
|
61
|
+
experts:
|
|
62
|
+
- name: powershell
|
|
63
|
+
source: { kind: hf, repo: SaeedRahmani/codeparrot_github_code_powershell, text_field: code }
|
|
64
|
+
- name: python
|
|
65
|
+
source: { kind: stack, language: Python }
|
|
66
|
+
# ...
|
|
67
|
+
|
|
68
|
+
budget:
|
|
69
|
+
target_steps: 150 # 1200 for a real rung; 150 is the shakedown
|
|
70
|
+
max_seq_length: 2048
|
|
71
|
+
per_device_batch: 4
|
|
72
|
+
grad_accum: 2
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
See `recipe.example.yaml` for the annotated version — every field carries the
|
|
76
|
+
measurement that chose it.
|
|
77
|
+
|
|
78
|
+
Budgets are in **tokens**, derived from steps. Capping documents instead looked
|
|
79
|
+
like balance and wasn't: at 10,000 documents each, PowerShell received 4.3× the
|
|
80
|
+
gradient updates Shell did, and a different LR curve besides.
|
|
81
|
+
|
|
82
|
+
## Refusals — read this bit
|
|
83
|
+
|
|
84
|
+
Right now `ms-moe-maker build` drives an existing pipeline script by setting
|
|
85
|
+
environment variables and forking it. That script exposes sixteen levers. A
|
|
86
|
+
recipe declares far more than sixteen things.
|
|
87
|
+
|
|
88
|
+
So the naive wrapper would accept `per_device_batch: 8`, run the build at 4,
|
|
89
|
+
and report success — leaving you with a document that *looks* authoritative and
|
|
90
|
+
silently isn't. That's the worst possible place to put that trap, in the one
|
|
91
|
+
file whose entire selling point is reproducing someone else's run.
|
|
92
|
+
|
|
93
|
+
**So a recipe field is honoured, or the build refuses. Never ignored.**
|
|
94
|
+
|
|
95
|
+
The check isn't "is there a lever" — it's "will the run actually do what the
|
|
96
|
+
document says". Ms.MoE reads the pipeline's own constants statically (via
|
|
97
|
+
`ast`, never importing — importing it would cost you a CUDA context) and
|
|
98
|
+
compares each field against the value that will really be used. Agreement is
|
|
99
|
+
silence. Only disagreement refuses.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
$ ms-moe-maker build recipe.yaml
|
|
103
|
+
2 recipe field(s) cannot be honoured by fraunkenstein_universal.py:
|
|
104
|
+
· budget.per_device_batch=8 cannot be applied: the pipeline uses 4 from
|
|
105
|
+
PER_DEVICE_BATCH and exposes no environment lever for it.
|
|
106
|
+
· gates.main_evals='manual' cannot be honoured: the pipeline runs end to
|
|
107
|
+
end with no stage boundary a gate could pause at.
|
|
108
|
+
REFUSED - nothing was run.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--allow-refusals` proceeds anyway. The refusals are recorded in the run
|
|
112
|
+
manifest either way, because the person who needs to know a lever was ignored
|
|
113
|
+
is the one reading the dashboard six hours later, not the one who saw the
|
|
114
|
+
terminal at kickoff.
|
|
115
|
+
|
|
116
|
+
**The refusal list is the roadmap.** Each entry is a field somebody wanted to
|
|
117
|
+
set and couldn't — which is exactly the priority order for pulling that part of
|
|
118
|
+
the script into a real stage. When the list is empty, the decomposition is
|
|
119
|
+
finished, and nobody had to guess when.
|
|
120
|
+
|
|
121
|
+
## The run manifest
|
|
122
|
+
|
|
123
|
+
A build writes `msmoe-run.json` into its run directory: what the run is, the
|
|
124
|
+
ordered stage list, each stage's status and artifact, and any refusals.
|
|
125
|
+
|
|
126
|
+
That file is the **only** interface between this package and any viewer.
|
|
127
|
+
Nothing imports anything. `seren-theatre` reads the manifest when it's there
|
|
128
|
+
and falls back to reading the directory when it isn't — so an instrumented run
|
|
129
|
+
is exact and an uninstrumented directory still works. Neither package is
|
|
130
|
+
required, and neither knows the other exists.
|
|
131
|
+
|
|
132
|
+
## Events
|
|
133
|
+
|
|
134
|
+
Under `--json`: one JSON object per line on stdout, prose on stderr, never
|
|
135
|
+
interleaved.
|
|
136
|
+
|
|
137
|
+
| event | when |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `started` | the build begins; carries the resolved env and run dir |
|
|
140
|
+
| `stage` | a stage changes status |
|
|
141
|
+
| `progress` | something worth knowing inside a stage |
|
|
142
|
+
| `refused` | recipe fields that couldn't be honoured |
|
|
143
|
+
| `warning` / `error` | trouble |
|
|
144
|
+
| `done` | terminal, with `ok` |
|
|
145
|
+
|
|
146
|
+
Every line is flushed. A consumer following a six-hour build through a pipe
|
|
147
|
+
would otherwise see nothing until the buffer filled — and that looks exactly
|
|
148
|
+
like a hang.
|
|
149
|
+
|
|
150
|
+
## Status
|
|
151
|
+
|
|
152
|
+
The stage machinery, contract and CLI are real. The pipeline itself is still
|
|
153
|
+
the original 2483-line script, driven from the outside — **wrap-then-carve**.
|
|
154
|
+
The contract is the product; the internals move behind it without anything
|
|
155
|
+
downstream noticing.
|
|
156
|
+
|
|
157
|
+
## Licence
|
|
158
|
+
|
|
159
|
+
GPL-3.0-only.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Ms.MoE - Multi-Specified Mixture of Experts.
|
|
2
|
+
|
|
3
|
+
Five deliberate experts instead of a hundred lottery tickets. The design thesis
|
|
4
|
+
is the INVERSE of a frontier MoE: hand-assign the domains so every expert has a
|
|
5
|
+
guaranteed constituency, which eliminates dead and collapsed experts by
|
|
6
|
+
construction rather than fighting them with a load-balancing auxiliary loss.
|
|
7
|
+
Because each expert does exactly one thing, you can retrain ONE and re-splice
|
|
8
|
+
without touching the others - which is what makes it maintainable by one
|
|
9
|
+
person.
|
|
10
|
+
|
|
11
|
+
The real product is the factory, not the model. Swap the expert list and
|
|
12
|
+
someone else gets their own Ms.MoE, shaped like THEIR stack.
|
|
13
|
+
|
|
14
|
+
This package deliberately depends on nothing of Seren's. seren-theatre can
|
|
15
|
+
watch a run, and seren-theatre[stagehand] can start one, but the arrow only
|
|
16
|
+
points that way - and even then the two never speak, they share a directory.
|
|
17
|
+
Opt in, never opt out.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from ._version import version as __version__
|
|
23
|
+
except Exception: # noqa: BLE001 - source checkout without a build
|
|
24
|
+
__version__ = "0.0.0+unknown"
|
|
25
|
+
|
|
26
|
+
from ._describe import DESCRIBE, NAME # noqa: F401 (stdlib-only, safe here)
|
|
27
|
+
|
|
28
|
+
__all__ = ["DESCRIBE", "NAME", "__version__"]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""The ms-moe-maker CLI - the command stagehand forks and a person types.
|
|
2
|
+
|
|
3
|
+
Three verbs:
|
|
4
|
+
|
|
5
|
+
ms-moe-maker describe one line of JSON, exit 0, no side effects
|
|
6
|
+
ms-moe-maker validate recipe.yaml parse + check, touching nothing
|
|
7
|
+
ms-moe-maker build recipe.yaml translate, fork the pipeline, report
|
|
8
|
+
|
|
9
|
+
`ms-moe-maker build recipe.yaml` is deliberately the literal string in the README and
|
|
10
|
+
the literal string seren-theatre[stagehand] forks. Not a Python API call, not
|
|
11
|
+
an internal entry point with different defaults - the same command. If the two
|
|
12
|
+
ever diverge, the hand-run path is the one that rots, because it is the one
|
|
13
|
+
with no automated users; making them identical removes the possibility.
|
|
14
|
+
|
|
15
|
+
--describe is scanned before argparse for the same reason every Seren installer
|
|
16
|
+
does it: it has to answer on a broken install, so nothing may run first.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from ._describe import DESCRIBE
|
|
27
|
+
from .events import Events
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _force_utf8_stdio() -> None:
|
|
31
|
+
"""UTF-8 regardless of console codepage. Windows defaults to legacy, and
|
|
32
|
+
the pipeline prints emoji milestones - a UnicodeEncodeError mid-build would
|
|
33
|
+
kill a run over a decorative character."""
|
|
34
|
+
for stream in (sys.stdout, sys.stderr):
|
|
35
|
+
try:
|
|
36
|
+
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
37
|
+
except (AttributeError, ValueError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _find_pipeline(explicit: str | None, recipe_path: Path,
|
|
42
|
+
required: bool) -> Path | None:
|
|
43
|
+
"""Locate fraunkenstein_universal.py. None if absent and not required.
|
|
44
|
+
|
|
45
|
+
Looked up rather than assumed, and reported when missing, because "wrapped
|
|
46
|
+
a script that isn't there" should be one clear error and not a traceback
|
|
47
|
+
from subprocess. Order: --pipeline, beside the recipe, cwd, then upward.
|
|
48
|
+
|
|
49
|
+
`required` is the difference between the two verbs, and it is not a
|
|
50
|
+
convenience:
|
|
51
|
+
|
|
52
|
+
build - REQUIRED. There is nothing to fork without it.
|
|
53
|
+
validate - OPTIONAL. The README promises `ms-moe-maker validate` runs on a
|
|
54
|
+
laptop with no GPU so you can check a recipe BEFORE going
|
|
55
|
+
near a machine that can run it. Demanding the pipeline made
|
|
56
|
+
that promise false: a stranger with a recipe and no checkout
|
|
57
|
+
got "could not find fraunkenstein_universal.py" and no
|
|
58
|
+
validation at all. Recipe SHAPE is checkable on its own; only
|
|
59
|
+
the refusal analysis needs a pipeline to compare against.
|
|
60
|
+
|
|
61
|
+
An explicit --pipeline that does not exist is always an error, for either
|
|
62
|
+
verb. Being told where it is and being wrong is different from not saying.
|
|
63
|
+
"""
|
|
64
|
+
from .levers import DEFAULT_PIPELINE
|
|
65
|
+
|
|
66
|
+
if explicit:
|
|
67
|
+
p = Path(explicit)
|
|
68
|
+
if not p.is_file():
|
|
69
|
+
raise SystemExit(f"--pipeline {p} does not exist")
|
|
70
|
+
return p.resolve()
|
|
71
|
+
|
|
72
|
+
for candidate in (recipe_path.parent / DEFAULT_PIPELINE,
|
|
73
|
+
Path.cwd() / DEFAULT_PIPELINE):
|
|
74
|
+
if candidate.is_file():
|
|
75
|
+
return candidate.resolve()
|
|
76
|
+
for parent in [Path.cwd(), *Path.cwd().parents]:
|
|
77
|
+
candidate = parent / DEFAULT_PIPELINE
|
|
78
|
+
if candidate.is_file():
|
|
79
|
+
return candidate.resolve()
|
|
80
|
+
if required:
|
|
81
|
+
raise SystemExit(
|
|
82
|
+
f"could not find {DEFAULT_PIPELINE}. Pass --pipeline PATH, or run "
|
|
83
|
+
f"from the directory that holds it.")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main(argv: list[str] | None = None) -> int:
|
|
88
|
+
_force_utf8_stdio()
|
|
89
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
90
|
+
|
|
91
|
+
# Before argparse: zero side effects, works half-installed.
|
|
92
|
+
if "--describe" in argv or (argv and argv[0] == "describe"):
|
|
93
|
+
print(json.dumps(DESCRIBE))
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
ap = argparse.ArgumentParser(
|
|
97
|
+
prog="ms-moe-maker",
|
|
98
|
+
description="Build a mixture of experts from a recipe.")
|
|
99
|
+
ap.add_argument("command", choices=["build", "validate", "describe"])
|
|
100
|
+
ap.add_argument("recipe", nargs="?", help="path to the recipe .yaml")
|
|
101
|
+
ap.add_argument("--pipeline", default=None,
|
|
102
|
+
help="path to fraunkenstein_universal.py (default: found "
|
|
103
|
+
"beside the recipe, then upward from cwd)")
|
|
104
|
+
ap.add_argument("--json", action="store_true",
|
|
105
|
+
help="JSON Lines events on stdout, prose on stderr")
|
|
106
|
+
ap.add_argument("--python", default=None,
|
|
107
|
+
help="interpreter to run the pipeline with (default: the "
|
|
108
|
+
"one running ms-moe-maker). Use this when the trainer lives "
|
|
109
|
+
"in a different venv - which is the normal case, "
|
|
110
|
+
"since ms-moe-maker is deliberately small and torch is not.")
|
|
111
|
+
ap.add_argument("--dryrun", action="store_true",
|
|
112
|
+
help="FRAUNK_DRYRUN=1 - the whole pipeline, small")
|
|
113
|
+
ap.add_argument("--force", action="store_true",
|
|
114
|
+
help="FRAUNK_FORCE=1 - redo stages whose artifacts exist")
|
|
115
|
+
ap.add_argument("--allow-refusals", action="store_true",
|
|
116
|
+
help="run even though some recipe fields cannot be "
|
|
117
|
+
"honoured. They are recorded in the manifest either "
|
|
118
|
+
"way; this only removes the stop.")
|
|
119
|
+
a = ap.parse_args(argv)
|
|
120
|
+
|
|
121
|
+
ev = Events(enabled=a.json)
|
|
122
|
+
|
|
123
|
+
if a.command == "describe":
|
|
124
|
+
print(json.dumps(DESCRIBE))
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
if not a.recipe:
|
|
128
|
+
ap.error("a recipe path is required")
|
|
129
|
+
|
|
130
|
+
from .recipe import load, resolve, validate
|
|
131
|
+
|
|
132
|
+
recipe_path = Path(a.recipe).resolve()
|
|
133
|
+
try:
|
|
134
|
+
rec, parse_warns = load(str(recipe_path))
|
|
135
|
+
except Exception as exc: # noqa: BLE001 - the message IS the product
|
|
136
|
+
ev.error("parse", str(exc))
|
|
137
|
+
ev.say(f"FAILED to parse {recipe_path}: {exc}")
|
|
138
|
+
return 2
|
|
139
|
+
|
|
140
|
+
errs, warns = validate(rec)
|
|
141
|
+
warns = parse_warns + warns
|
|
142
|
+
for w in warns:
|
|
143
|
+
ev.warning(w)
|
|
144
|
+
ev.say(f" WARN {w}")
|
|
145
|
+
for e in errs:
|
|
146
|
+
ev.error("validate", e)
|
|
147
|
+
ev.say(f" ERROR {e}")
|
|
148
|
+
if errs:
|
|
149
|
+
ev.done(ok=False, stage="validate")
|
|
150
|
+
return 1
|
|
151
|
+
|
|
152
|
+
# RESOLVED BEFORE THE PIPELINE LOOKUP, on purpose. Both are explicit
|
|
153
|
+
# arguments, and an explicit argument that is wrong should say so no matter
|
|
154
|
+
# what else is also missing - same rule as --pipeline. Reporting "could not
|
|
155
|
+
# find fraunkenstein_universal.py" to someone who mistyped --python sends
|
|
156
|
+
# them to fix the wrong thing.
|
|
157
|
+
#
|
|
158
|
+
# MSMOE_PYTHON as well as --python: the interpreter is a property of the
|
|
159
|
+
# BOX, not of the run, so it belongs somewhere you set once. Same shape as
|
|
160
|
+
# the family's SEREN_<X>_* levers - a flag for the one-off, an env var for
|
|
161
|
+
# the machine.
|
|
162
|
+
interpreter = a.python or os.environ.get("MSMOE_PYTHON") or None
|
|
163
|
+
if interpreter:
|
|
164
|
+
ipath = Path(interpreter)
|
|
165
|
+
if not ipath.is_file():
|
|
166
|
+
raise SystemExit(f"--python {ipath} does not exist")
|
|
167
|
+
# abspath, NEVER resolve(). A venv's bin/python is a SYMLINK to the
|
|
168
|
+
# base interpreter, and resolving it throws the venv away: you asked
|
|
169
|
+
# for /lab/bin/python and got /usr/bin/python3.12, whose sys.prefix is
|
|
170
|
+
# /usr and whose site-packages has none of your training deps. The
|
|
171
|
+
# failure then reads as "No module named 'torch'" from an interpreter
|
|
172
|
+
# you never named, which is about as misleading as it gets.
|
|
173
|
+
#
|
|
174
|
+
# Measured: running the symlink gives sys.prefix=/tmp/venvtest;
|
|
175
|
+
# running its target gives sys.prefix=/usr. Same file, different venv.
|
|
176
|
+
# abspath normalises the path without following the link.
|
|
177
|
+
interpreter = os.path.abspath(str(ipath))
|
|
178
|
+
|
|
179
|
+
pipeline = _find_pipeline(a.pipeline, recipe_path,
|
|
180
|
+
required=(a.command == "build"))
|
|
181
|
+
|
|
182
|
+
from .levers import Translation, translate
|
|
183
|
+
|
|
184
|
+
if pipeline is None:
|
|
185
|
+
# Recipe-only validation. Say so LOUDLY rather than reporting a clean
|
|
186
|
+
# bill of health: "valid" and "valid, and nothing checked whether the
|
|
187
|
+
# pipeline can honour it" are different answers, and quietly giving
|
|
188
|
+
# the first when you mean the second is how a document that lies gets
|
|
189
|
+
# blessed on its way out the door.
|
|
190
|
+
tr = Translation()
|
|
191
|
+
no_pipeline_note = (
|
|
192
|
+
"no pipeline found, so ONLY the recipe's own shape was checked. "
|
|
193
|
+
"Refusals could not be computed - run this again beside "
|
|
194
|
+
"fraunkenstein_universal.py, or pass --pipeline PATH, to find out "
|
|
195
|
+
"whether a build would actually honour these fields.")
|
|
196
|
+
ev.warning(no_pipeline_note)
|
|
197
|
+
else:
|
|
198
|
+
no_pipeline_note = ""
|
|
199
|
+
tr = translate(rec, pipeline, force=a.force)
|
|
200
|
+
|
|
201
|
+
if tr.refusals:
|
|
202
|
+
ev.refused(tr.refusals)
|
|
203
|
+
ev.say("")
|
|
204
|
+
ev.say(f" {len(tr.refusals)} recipe field(s) cannot be honoured by "
|
|
205
|
+
f"{pipeline.name}:")
|
|
206
|
+
for r in tr.refusals:
|
|
207
|
+
ev.say(f" · {r}")
|
|
208
|
+
ev.say("")
|
|
209
|
+
ev.say(" These are not warnings. A recipe is a document you hand to "
|
|
210
|
+
"someone so they get YOUR run, and a field that is silently "
|
|
211
|
+
"ignored makes it a document that lies. Fix the recipe, carve "
|
|
212
|
+
"the stage out, or pass --allow-refusals to proceed knowing "
|
|
213
|
+
"the build will not match the file.")
|
|
214
|
+
|
|
215
|
+
if a.command == "validate":
|
|
216
|
+
eff = resolve(rec)
|
|
217
|
+
ev.emit("resolved", **eff)
|
|
218
|
+
ev.say("")
|
|
219
|
+
ev.say(f"Ms.MoE recipe {rec.name} [{eff['recipe_id']}]")
|
|
220
|
+
ev.say(f" pipeline {pipeline if pipeline else '(none found)'}")
|
|
221
|
+
ev.say(f" honoured {len(tr.agreed)} field(s), "
|
|
222
|
+
f"{len(tr.env)} env lever(s) set")
|
|
223
|
+
ev.say(f" refused {len(tr.refusals)} field(s)")
|
|
224
|
+
if no_pipeline_note:
|
|
225
|
+
ev.say("")
|
|
226
|
+
ev.say(f" NOTE {no_pipeline_note}")
|
|
227
|
+
ok = not tr.refusals
|
|
228
|
+
ev.done(ok=ok, refusals=len(tr.refusals), agreed=len(tr.agreed),
|
|
229
|
+
env=tr.env, pipeline=str(pipeline) if pipeline else None,
|
|
230
|
+
# The consumer needs to be able to tell "no refusals" from
|
|
231
|
+
# "refusals were never computed". Same key set either way, one
|
|
232
|
+
# honest flag - the alternative is a caller inferring depth of
|
|
233
|
+
# analysis from an empty list, which it cannot do.
|
|
234
|
+
refusals_checked=pipeline is not None)
|
|
235
|
+
return 0 if ok else 1
|
|
236
|
+
|
|
237
|
+
# build
|
|
238
|
+
if tr.refusals and not a.allow_refusals:
|
|
239
|
+
ev.done(ok=False, stage="translate", refusals=len(tr.refusals))
|
|
240
|
+
ev.say(" REFUSED - nothing was run.")
|
|
241
|
+
return 3
|
|
242
|
+
|
|
243
|
+
from .runner import Runner
|
|
244
|
+
|
|
245
|
+
if interpreter:
|
|
246
|
+
ev.say(f" pipeline interpreter: {interpreter}")
|
|
247
|
+
runner = Runner(rec, pipeline, tr, ev, cwd=pipeline.parent,
|
|
248
|
+
dryrun=a.dryrun, python=interpreter)
|
|
249
|
+
return runner.run()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
raise SystemExit(main())
|