moonclip 0.0.6__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.
- moonclip-0.0.6/.gitignore +82 -0
- moonclip-0.0.6/CHANGELOG.md +224 -0
- moonclip-0.0.6/Cargo.lock +1178 -0
- moonclip-0.0.6/Cargo.toml +73 -0
- moonclip-0.0.6/LICENSE +201 -0
- moonclip-0.0.6/PKG-INFO +219 -0
- moonclip-0.0.6/README.md +186 -0
- moonclip-0.0.6/benches/delta_bench.rs +93 -0
- moonclip-0.0.6/pyproject.toml +62 -0
- moonclip-0.0.6/python/moonclip/__init__.py +23 -0
- moonclip-0.0.6/python/moonclip/__init__.pyi +8 -0
- moonclip-0.0.6/python/moonclip/_env.py +41 -0
- moonclip-0.0.6/python/moonclip/moonclip.pyi +224 -0
- moonclip-0.0.6/python/moonclip/py.typed +0 -0
- moonclip-0.0.6/python/moonclip/pytorch.py +1079 -0
- moonclip-0.0.6/python/moonclip/pytorch.pyi +149 -0
- moonclip-0.0.6/requirements.txt +5 -0
- moonclip-0.0.6/src/cast.rs +721 -0
- moonclip-0.0.6/src/compression.rs +251 -0
- moonclip-0.0.6/src/coordinator.rs +3038 -0
- moonclip-0.0.6/src/delta.rs +409 -0
- moonclip-0.0.6/src/error.rs +37 -0
- moonclip-0.0.6/src/hash.rs +42 -0
- moonclip-0.0.6/src/inflight.rs +180 -0
- moonclip-0.0.6/src/lib.rs +19 -0
- moonclip-0.0.6/src/manifest.rs +440 -0
- moonclip-0.0.6/src/merger.rs +1396 -0
- moonclip-0.0.6/src/pack.rs +196 -0
- moonclip-0.0.6/src/pool.rs +180 -0
- moonclip-0.0.6/src/profile.rs +146 -0
- moonclip-0.0.6/src/python.rs +483 -0
- moonclip-0.0.6/src/remote_sync.rs +577 -0
- moonclip-0.0.6/src/s3.rs +815 -0
- moonclip-0.0.6/src/shuffle.rs +181 -0
- moonclip-0.0.6/src/storage.rs +387 -0
- moonclip-0.0.6/src/tensor.rs +1246 -0
- moonclip-0.0.6/tests/s3_minio.rs +201 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/target
|
|
2
|
+
# Generated benchmark checkpoints (~4 GB), not the benchmark sources. The
|
|
3
|
+
# rule used to be /bench*, which also swallowed benches/ and bench/ — source
|
|
4
|
+
# directories that belong in the repo.
|
|
5
|
+
/bench_baseline/
|
|
6
|
+
|
|
7
|
+
# Byte-compiled / optimized / DLL files
|
|
8
|
+
__pycache__/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
|
|
12
|
+
# Native extensions
|
|
13
|
+
*.so
|
|
14
|
+
*.pyd
|
|
15
|
+
*.pdb
|
|
16
|
+
|
|
17
|
+
# Distribution / packaging
|
|
18
|
+
.Python
|
|
19
|
+
.venv/
|
|
20
|
+
env/
|
|
21
|
+
bin/
|
|
22
|
+
build/
|
|
23
|
+
develop-eggs/
|
|
24
|
+
dist/
|
|
25
|
+
eggs/
|
|
26
|
+
lib/
|
|
27
|
+
lib64/
|
|
28
|
+
parts/
|
|
29
|
+
sdist/
|
|
30
|
+
var/
|
|
31
|
+
include/
|
|
32
|
+
man/
|
|
33
|
+
venv/
|
|
34
|
+
*.egg-info/
|
|
35
|
+
.installed.cfg
|
|
36
|
+
*.egg
|
|
37
|
+
|
|
38
|
+
# Installer logs
|
|
39
|
+
pip-log.txt
|
|
40
|
+
pip-delete-this-directory.txt
|
|
41
|
+
pip-selfcheck.json
|
|
42
|
+
|
|
43
|
+
# Unit test / coverage reports
|
|
44
|
+
htmlcov/
|
|
45
|
+
.tox/
|
|
46
|
+
.coverage
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
|
|
51
|
+
# Translations
|
|
52
|
+
*.mo
|
|
53
|
+
|
|
54
|
+
# Mr Developer
|
|
55
|
+
.mr.developer.cfg
|
|
56
|
+
.project
|
|
57
|
+
.pydevproject
|
|
58
|
+
|
|
59
|
+
# Rope
|
|
60
|
+
.ropeproject
|
|
61
|
+
|
|
62
|
+
# Django stuff:
|
|
63
|
+
*.log
|
|
64
|
+
*.pot
|
|
65
|
+
|
|
66
|
+
.DS_Store
|
|
67
|
+
|
|
68
|
+
# Sphinx documentation
|
|
69
|
+
docs/_build/
|
|
70
|
+
|
|
71
|
+
# PyCharm
|
|
72
|
+
.idea/
|
|
73
|
+
|
|
74
|
+
# VSCode
|
|
75
|
+
.vscode/
|
|
76
|
+
|
|
77
|
+
# Pyenv
|
|
78
|
+
.python-version
|
|
79
|
+
|
|
80
|
+
# Training artifacts
|
|
81
|
+
moonclip_*_ckpts/
|
|
82
|
+
.claude
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.6 — unreleased
|
|
4
|
+
|
|
5
|
+
Everything an external review of the 0.0.4 artifacts found, plus the four things
|
|
6
|
+
that turned up while fixing them. Each entry below has a regression test that
|
|
7
|
+
fails on 0.0.4.
|
|
8
|
+
|
|
9
|
+
**There is no 0.0.5.** It was prepared, merged and never published: the review
|
|
10
|
+
that produced the rest of this list arrived first, and shipping the small half
|
|
11
|
+
on its own would have meant a release whose headline fix was a warning about a
|
|
12
|
+
bug that is now fixed. Its entries are folded in below.
|
|
13
|
+
|
|
14
|
+
Nothing here changes the pack format. A 0.0.4 checkpoint reads unchanged, and
|
|
15
|
+
one written by 0.0.6 is readable by 0.0.4 — with the same bugs.
|
|
16
|
+
|
|
17
|
+
### Fixed — data loss
|
|
18
|
+
|
|
19
|
+
- **Merging dropped tensors the model gained and resurrected ones it lost.**
|
|
20
|
+
`do_full_merge` rebuilt the merged snapshot from the **base** snapshot's
|
|
21
|
+
tensor list, so the model's shape at the base decided what survived the fold.
|
|
22
|
+
An adapter added mid-run, a growing head, a pruned layer: the merged
|
|
23
|
+
checkpoint loaded without complaint, holding the wrong set of parameters. It
|
|
24
|
+
walks the newest delta now — that list *is* the state dict at the step the
|
|
25
|
+
merge stands in for, and it is what a load of that snapshot would return.
|
|
26
|
+
Two more defects came with it: intermediate deltas were replayed in sequence,
|
|
27
|
+
which corrupted any tensor that changed and changed back (the newest delta
|
|
28
|
+
says "identical to the base" and the replay left the intermediate value), and
|
|
29
|
+
`original_dtype` was dropped, so merging a `save_dtype="bf16"` checkpoint
|
|
30
|
+
quietly stopped uncasting it on load. Merging is off by default
|
|
31
|
+
(`merge_stride=0`), so this only ever reached people who turned it on.
|
|
32
|
+
- **A merge could delete the base a save was reading.** `save_sync` takes its
|
|
33
|
+
base from the manifest and releases the lock before reading it; the merger
|
|
34
|
+
holds the lock only while deciding what to fold. Overlapping those windows
|
|
35
|
+
failed the save outright (`Checkpoint not found: snapshots/…/rank_0.pack`)
|
|
36
|
+
or, worse, published a delta pointing at a base the merge had already
|
|
37
|
+
removed — broken on the next `load_latest`, and discarded by startup recovery
|
|
38
|
+
as an orphan. There is now a registry of what is being read
|
|
39
|
+
(`crate::inflight`): a merge does not consume a snapshot somebody is inside,
|
|
40
|
+
and a save does not pick up a base a merge has claimed. Neither side waits
|
|
41
|
+
for the other — the merge stands down and runs on the next notify, the save
|
|
42
|
+
writes a full snapshot for that one step.
|
|
43
|
+
- **Multi-rank saves lost each other's shards.** Every rank reloaded
|
|
44
|
+
`manifest.json`, inserted its own entry and wrote the whole file back — a
|
|
45
|
+
read-modify-write with no lock, across processes. The Python layer hid it by
|
|
46
|
+
serialising the ranks behind a barrier, which made the README's "each rank
|
|
47
|
+
saves its own shard independently" false and a multi-GPU save sequential. A
|
|
48
|
+
rank now writes only its own pack; `finalize_snapshot` assembles the snapshot
|
|
49
|
+
from the descriptors in those packs. One writer, no lock, and the shards go
|
|
50
|
+
in parallel.
|
|
51
|
+
|
|
52
|
+
### Fixed — checkpoints that could not be read or recovered
|
|
53
|
+
|
|
54
|
+
- **Tied weights became unreadable when the tensor order changed.**
|
|
55
|
+
Deduplication stores the first of a set of identical tensors and records the
|
|
56
|
+
rest as aliases of it. Which one comes first is the state dict's iteration
|
|
57
|
+
order, and wrapping a model differently is enough to swap them: the tensor
|
|
58
|
+
that used to be the alias then carries the bytes, is unchanged since the
|
|
59
|
+
base, and is written `Skipped` — and resolving that skip landed on the base's
|
|
60
|
+
alias entry, which holds no bytes. `Tensor 'head' is an alias of 'emb' and
|
|
61
|
+
must be resolved after the snapshot's other tensors`, on a checkpoint that
|
|
62
|
+
was completely intact. The base lookup follows the reference now.
|
|
63
|
+
- **A checkpoint where nothing changed could not be recovered.** With every
|
|
64
|
+
tensor skipped there are no bytes to write, so no pack was written — and the
|
|
65
|
+
pack is what carries the descriptor that startup recovery rebuilds a lost
|
|
66
|
+
snapshot from. A process killed between the save and the manifest write left
|
|
67
|
+
that checkpoint unrecoverable, in the case that is ordinary rather than
|
|
68
|
+
exotic: fine-tuning, where whole stretches of the model do not move. A pack
|
|
69
|
+
holding just the descriptor is always written now, a few hundred bytes.
|
|
70
|
+
- **No `fsync` anywhere in the write path.** The temp-file-then-rename was
|
|
71
|
+
atomic for readers and said nothing about power: the rename could reach the
|
|
72
|
+
disk while the data was still in the page cache, leaving a pack of exactly
|
|
73
|
+
the right length full of zeros, vouched for by the manifest. `flush()`
|
|
74
|
+
documented "durably on disk" and did not deliver it. The data is now synced
|
|
75
|
+
before the rename and the directory after it. `MOONCLIP_FSYNC=0` restores the
|
|
76
|
+
old behaviour where the checkpoint is not the thing being protected.
|
|
77
|
+
|
|
78
|
+
### Fixed — quietly wrong
|
|
79
|
+
|
|
80
|
+
- **The fp32→bf16 cast could turn a NaN into +Inf.** bf16 keeps the top 7
|
|
81
|
+
mantissa bits, so a NaN whose payload sits below bit 16 — `0x7F800001`, which
|
|
82
|
+
is what comparisons against a corrupted tensor tend to produce — reached the
|
|
83
|
+
truncation with a zero mantissa under an all-ones exponent, and that is
|
|
84
|
+
infinity. The quiet NaN `0x7FC00000` was never affected, which is why no test
|
|
85
|
+
caught it. NaN is mapped to a NaN explicitly now, sign and the surviving
|
|
86
|
+
payload bits kept. It only ever affected `save_dtype="bf16"`, and what it
|
|
87
|
+
cost was the evidence: a diverged run saved looking finite.
|
|
88
|
+
- **`max_rollback_snapshots` was never read.** Defined, defaulted, documented
|
|
89
|
+
and passed in from Python, and no code consulted it — so every snapshot that
|
|
90
|
+
ever landed on `rollback_interval_steps` kept its exemption from retention
|
|
91
|
+
forever and the store grew without bound. Only the newest N are protected
|
|
92
|
+
now; past that they become ordinary snapshots again and retention prunes them
|
|
93
|
+
in its own order. `0` disables rollback protection, as it already did for the
|
|
94
|
+
interval.
|
|
95
|
+
- **`save_final()` uploaded before the merge finished.** `merge_now()` queued a
|
|
96
|
+
merge on another thread and returned; `sync_now()` waited only for the save
|
|
97
|
+
thread, so the final merged snapshot — the one every earlier checkpoint had
|
|
98
|
+
just been folded into — could still be being written when the upload listed
|
|
99
|
+
the files. `merge_now()` blocks until the merge is done and reports its
|
|
100
|
+
failure.
|
|
101
|
+
- **Deletions never reached the remote.** Retention and the merger deleted
|
|
102
|
+
locally and stopped there, so `keep_last` bounded the SSD while the bucket
|
|
103
|
+
kept every pack the run ever wrote. Deleted keys are now queued and removed
|
|
104
|
+
on the syncer's next pass. Only keys this process deleted: listing the remote
|
|
105
|
+
and removing whatever is not local would erase a backup the first time a
|
|
106
|
+
fresh machine pointed at it.
|
|
107
|
+
- **A typo in `save_dtype` was silently ignored.** `DType::from_str` mapped
|
|
108
|
+
anything it did not recognise to "do not cast", so `save_dtype="bfloat"`
|
|
109
|
+
produced full-precision checkpoints and said nothing. It is `DType::parse`
|
|
110
|
+
now and returns an error.
|
|
111
|
+
- **`save_dtype` skipped float64, float16 and bfloat16 sources.**
|
|
112
|
+
`is_castable_float` declared them castable and `cast_tensor` had no arm for
|
|
113
|
+
them, so those tensors were stored untouched at the size the setting was
|
|
114
|
+
chosen to avoid. Every float dtype now converts to every other, through fp32.
|
|
115
|
+
- **An unknown dtype on load became float32.** `_DTYPE_MAP.get(dtype,
|
|
116
|
+
torch.float32)` reinterpreted the bytes under a dtype that was not theirs
|
|
117
|
+
whenever the element sizes divided — right shape, plausible numbers, no
|
|
118
|
+
relationship to what was saved. It raises now, and `complex64`/`complex128`,
|
|
119
|
+
which the byte path had always been able to *save*, can finally be read back.
|
|
120
|
+
|
|
121
|
+
### Fixed — S3 backend
|
|
122
|
+
|
|
123
|
+
- 404 was recognised by looking for the substring `"404"` in an error message,
|
|
124
|
+
which also matched any object whose key contained those digits. The status is
|
|
125
|
+
carried in the error type now.
|
|
126
|
+
- No retries at all: one 503, one reset connection or one DNS hiccup failed a
|
|
127
|
+
checkpoint upload. Three attempts with backoff, for 5xx, 429 and transport
|
|
128
|
+
errors only — a bad signature or a missing bucket still fails immediately.
|
|
129
|
+
- `ListObjectsV2` keys were used without unescaping XML entities, so a key
|
|
130
|
+
containing `&` came back as `a&b`. Every later read or delete used a key
|
|
131
|
+
that does not exist: the object was unreachable and still billed.
|
|
132
|
+
- `get_range` fell through to the trait default, which downloads the whole
|
|
133
|
+
object and slices it — the entire checkpoint over the network, once per
|
|
134
|
+
tensor. It sends a `Range` header now.
|
|
135
|
+
|
|
136
|
+
### Fixed — packaging
|
|
137
|
+
|
|
138
|
+
- **`requirements.txt` was UTF-16LE**, so `pip install -r requirements.txt`
|
|
139
|
+
failed to parse it. UTF-8 now.
|
|
140
|
+
- **Wheels carried `__pycache__`.** Every 0.0.4 wheel shipped cp314 bytecode
|
|
141
|
+
from a working tree where the tests had already run: harmless, and six wheels
|
|
142
|
+
for six interpreters holding one interpreter's `.pyc` files.
|
|
143
|
+
- **The `Development Status` classifier claimed `5 - Production/Stable`** on a
|
|
144
|
+
0.0.x release. It is `4 - Beta`.
|
|
145
|
+
|
|
146
|
+
### Added
|
|
147
|
+
|
|
148
|
+
- **A source distribution on PyPI.** Wheels are still Linux x86_64 only, so
|
|
149
|
+
until now `pip install moonclip` on macOS, Windows or aarch64 answered "no
|
|
150
|
+
matching distribution found" — which reads as "this package does not exist"
|
|
151
|
+
rather than "there is no wheel for your platform". pip can build from the
|
|
152
|
+
sdist instead, given a Rust toolchain.
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- `merge_now()` blocks and returns a result instead of returning immediately.
|
|
157
|
+
- `DType::from_str` is `DType::parse` and returns `Result<DType>`.
|
|
158
|
+
- `RemoteSyncer::new` takes a `PendingDeletes` queue.
|
|
159
|
+
- `DeltaMerger::new` is crate-internal: a merger has to share the coordinator's
|
|
160
|
+
in-flight registry or it deletes snapshots out from under readers, and there
|
|
161
|
+
is no way to hand one in from outside.
|
|
162
|
+
- `is_castable_float` includes float64 again — this time because the cast
|
|
163
|
+
exists.
|
|
164
|
+
|
|
165
|
+
## 0.0.4 — 2026-08-17
|
|
166
|
+
|
|
167
|
+
First release on PyPI (`pip install moonclip`) and crates.io. Until now the only
|
|
168
|
+
way in was `pip install git+…`, so the entries below are written for whoever did
|
|
169
|
+
that — and the first one is a reason to move.
|
|
170
|
+
|
|
171
|
+
### Fixed
|
|
172
|
+
|
|
173
|
+
- **`keep_last` erased the history instead of pruning it.** The total cap could
|
|
174
|
+
only remove a whole snapshot *group* — a full plus every delta hanging off it.
|
|
175
|
+
Since every delta is computed against the last full, a normal run has one
|
|
176
|
+
group for thousands of steps, so any `keep_last` smaller than that group did
|
|
177
|
+
not prune: it emptied the store. Found on real hardware with `keep_last: 2`,
|
|
178
|
+
three checkpoints delivered and nothing left after shutdown. The cap now drops
|
|
179
|
+
the oldest *delta*, one at a time; a full survives as long as some delta needs
|
|
180
|
+
it as a base, which means the oldest surviving step can be older than the cap
|
|
181
|
+
suggests. That is the honest answer instead of an unreadable checkpoint.
|
|
182
|
+
- **Merged snapshots were not recoverable.** `do_full_merge` wrote individual
|
|
183
|
+
files with no pack descriptor, so a snapshot produced by the merger could not
|
|
184
|
+
be recovered at startup — and the merge is exactly the moment when a run's
|
|
185
|
+
whole history is concentrated into one snapshot.
|
|
186
|
+
- **Loading a multi-rank snapshot could read another rank's shard.** The rank was
|
|
187
|
+
inferred from the tensor name, and under sharding every rank uses the same
|
|
188
|
+
names. It is passed explicitly now.
|
|
189
|
+
- **`print_stats()` could take down the process.** It drew a rule with U+2500,
|
|
190
|
+
which raises `UnicodeEncodeError` on a cp1252 console: a reporting call
|
|
191
|
+
interrupted training that was checkpointing perfectly well.
|
|
192
|
+
- **`merge_stride` did not do what its name says**, and the chain-depth limit was
|
|
193
|
+
checked after the stride, so a stride larger than `max_chain_depth` never hit
|
|
194
|
+
the ceiling.
|
|
195
|
+
- Type stubs were out of sync with the code (`flush` missing from
|
|
196
|
+
`CheckpointManager`, `keep_base_in_memory` undeclared) — which counts double
|
|
197
|
+
now that the package ships `py.typed`.
|
|
198
|
+
|
|
199
|
+
### Added
|
|
200
|
+
|
|
201
|
+
- **A private thread pool**, sized `cores / LOCAL_WORLD_SIZE`, with
|
|
202
|
+
`MOONCLIP_THREADS` overriding it. Previously every parallel pass used rayon's
|
|
203
|
+
global pool at one thread per core, so eight ranks on a 128-core node ran 128
|
|
204
|
+
threads each and competed with the application's own rayon. This is hygiene
|
|
205
|
+
rather than speed: capping the threads to the per-process quota measured
|
|
206
|
+
*slightly slower* on 8× RTX 5060 Ti (11.6 s against 10.6 s per handoff), and
|
|
207
|
+
the time turned out not to be inside this crate at all.
|
|
208
|
+
- `MOONCLIP_PROFILE=1` now also reports how long a save waited for the previous
|
|
209
|
+
one to drain. Moonclip keeps one write in flight, so from outside that wait is
|
|
210
|
+
indistinguishable from a slow copy.
|
|
211
|
+
- `py.typed`, so installed type checkers honour the shipped stubs.
|
|
212
|
+
|
|
213
|
+
### Known limits
|
|
214
|
+
|
|
215
|
+
- **Wheels are Linux x86_64 (manylinux_2_28) only**, CPython 3.9-3.14. Other
|
|
216
|
+
platforms have to build from source, which needs a Rust toolchain.
|
|
217
|
+
- **The speedup is 1.6-3.0×, not the 10-50× an early estimate suggested.** That
|
|
218
|
+
number was projected before anything was measured. On dense pre-training with
|
|
219
|
+
Adam every parameter changes at every step, so there is nothing to skip: what
|
|
220
|
+
is left is 1.6-3.0× faster than `torch.save` and 1.9× smaller. The 6-10× size
|
|
221
|
+
reduction remains plausible on fine-tuning, LoRA and adapters, where most
|
|
222
|
+
tensors are identical between snapshots — it has not been measured yet.
|
|
223
|
+
- Pack files written before 0.0.3 have no magic bytes: they stay readable through
|
|
224
|
+
the manifest, but cannot be recovered without it.
|