Rewrite model card in first person; ASCII hyphens only; keep all facts
Browse files
README.md
CHANGED
|
@@ -8,154 +8,95 @@ tags:
|
|
| 8 |
- webrtc
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
| 12 |
|
| 13 |
-
|
| 14 |
-
Model page (weights + card): https://huggingface.co/DaisyChainAI/DaisyChain-Train
|
| 15 |
|
| 16 |
-
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
> train neural networks. The training runs through **emulated GPU logic** —
|
| 20 |
-
> verified INT8 units (GUDA-style) that stand in for a GPU's math — so machines
|
| 21 |
-
> *without* a modern GPU can still do the work. Chain several together and they
|
| 22 |
-
> train one shared model as a cluster.
|
| 23 |
-
> Before you rely on it, see what it **can't** do → [Limitations](docs/LIMITS.md).
|
| 24 |
|
| 25 |
-
|
| 26 |
-
GPU logic (verified INT8 units — multiply / requantize / ReLU) to compute the
|
| 27 |
-
model, and DaisyChain pools the machines data-parallel: device selection,
|
| 28 |
-
capacity-weighted sharding, gradient sync, a P2P setup, and a live dashboard.
|
| 29 |
-
Two ways to run — **Docker** or **Python**.
|
| 30 |
|
| 31 |
-
--
|
| 32 |
|
| 33 |
-
##
|
| 34 |
-
DaisyChain-Train is for **small models on spare hardware**. It **pools compute,
|
| 35 |
-
not memory** (the model must fit on one machine), scaling is **sublinear**, and
|
| 36 |
-
it is **not** a substitute for a real GPU on large models. Full envelope in
|
| 37 |
-
**[docs/LIMITS.md](docs/LIMITS.md)** — please read it before relying on it.
|
| 38 |
|
| 39 |
-
-
|
| 40 |
|
| 41 |
## Feature list
|
| 42 |
|
| 43 |
### Python cluster trainer (`daisychain/`)
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
- **
|
| 48 |
-
|
| 49 |
-
- **
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
- **
|
| 53 |
-
via `DAISY_TASK`; template in `examples/my_task_template.py`.
|
| 54 |
-
- **Plain-float alternative task** — same cluster and pooling with ordinary
|
| 55 |
-
float math.
|
| 56 |
-
- **Live dashboard** (`daisychain-dashboard`) — readiness banner, P2P
|
| 57 |
-
connectivity scan, pooled cores/RAM, per-node capacity plan, live loss.
|
| 58 |
-
- **SpikeWhale control panel** (`spikewhale_panel`, `localhost:8899`) —
|
| 59 |
-
sliders for model size / training settings, any HF dataset you can access
|
| 60 |
-
(default streamed FineWeb-Edu), start/stop/re-adjust, live loss.
|
| 61 |
-
- **Docker demo cluster** — 3 nodes + dashboard in one command.
|
| 62 |
- **Windows helper** (`scripts\setup.bat`) and Tailscale mesh guide.
|
| 63 |
|
| 64 |
-
### DaisyChain-Web (`web/`)
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
- **Private cross-network rooms**
|
| 68 |
-
|
| 69 |
-
- **
|
| 70 |
-
|
| 71 |
-
- **
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
- **
|
| 75 |
-
|
| 76 |
-
- **
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
- **
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
- **Hardcoded FineWeb-Edu streaming** — the server reads random slices of
|
| 87 |
-
the 10BT parquet shards straight off the HF CDN via HTTP range requests
|
| 88 |
-
(pure-JS `hyparquet`); built-in corpus fallback offline.
|
| 89 |
-
- **Checkpoints** — download `.pt`, upload → **broadcast to the whole
|
| 90 |
-
group**; validated (magic, dims, tokenizer vocab) before accepting.
|
| 91 |
-
- **Inference kit** — one self-contained HTML file with the trained weights
|
| 92 |
-
baked in; generations offline, anywhere.
|
| 93 |
-
- **In-page generation** — prompt box on the trained model.
|
| 94 |
-
- **Old-hardware tier** — no WebGPU? The identical units run on CPU (same
|
| 95 |
-
bits, so CPU and GPU devices co-train in one group). There is no
|
| 96 |
-
plain-float path.
|
| 97 |
-
- **Large-message fragmentation** — multi-MB gradients/checkpoints chunked
|
| 98 |
-
at 48 KB over the data channels.
|
| 99 |
-
|
| 100 |
-
### Verified compute & kernels (web)
|
| 101 |
-
- **Verified INT8 units everywhere** — block-scaled int8 GEMM: exact LUT
|
| 102 |
-
products, exact int32 accumulation, bit-exact f32 epilogue with a pinned
|
| 103 |
-
rounding schedule; scales derived in JS f64 (division never runs on GPU).
|
| 104 |
-
- **Backends, best-first** — DP4A hardware int8 dot → LUT compute shader →
|
| 105 |
-
CPU mirror; every kernel **exact-gated at init** (bit-level compares) and
|
| 106 |
-
demoted to the mirror on any mismatch.
|
| 107 |
- **Continuous random-cell audit** at live training shapes.
|
| 108 |
-
- **Fused attention kernels**
|
| 109 |
-
|
| 110 |
-
- **
|
| 111 |
-
|
| 112 |
-
- **B2B MLP chain** — both MLP GEMMs back-to-back on GPU with fused per-row
|
| 113 |
-
absmax reduction and on-device quantize (ex. 13 + 23); WGSL-exact respec
|
| 114 |
-
with a fround-stepped JS mirror; fma-contraction-immune by construction.
|
| 115 |
-
- **Dispatch-optimized backward** — overlapped independent GEMMs, batch-3
|
| 116 |
-
sibling fusions (ex. 05/24); bit-identical gradients; optional int8 STE
|
| 117 |
-
backward path (dormant, 1.21× vs float).
|
| 118 |
|
| 119 |
### Verification stack (web)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
- **IEEE-754 binary32 oracle** in exact BigInt arithmetic
|
| 123 |
-
|
| 124 |
-
- **
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
- **RDNA2 ISA audit hardenings** — bit-level (−0-aware) gate comparisons;
|
| 128 |
-
proof that FMA contraction cannot change the quantize.
|
| 129 |
-
- **Twelve-suite test chain** (`cd web && npm test`) — convergence, replicas,
|
| 130 |
-
oracle, gates, properties, external corpus, **self-corpus** (the instruments
|
| 131 |
-
scored against my own bugs), B2B, optimizer, transformer LM, int8 backward,
|
| 132 |
-
non-finite quantize refusal;
|
| 133 |
-
results in [web/TEST_RESULTS.md](web/TEST_RESULTS.md).
|
| 134 |
-
- **Dirty-buffer gate** — the pool is poisoned before a re-sweep so state bugs
|
| 135 |
-
(a kernel assuming zeroed memory) are caught deterministically rather than
|
| 136 |
-
by ordering luck.
|
| 137 |
|
| 138 |
### Documentation
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
- Web: [Getting started](web/docs/GETTING_STARTED.md),
|
| 142 |
-
[Architecture](web/docs/ARCHITECTURE.md),
|
| 143 |
-
[Verification](web/docs/VERIFICATION.md),
|
| 144 |
-
[Troubleshooting](web/docs/TROUBLESHOOTING.md).
|
| 145 |
|
| 146 |
---
|
| 147 |
|
| 148 |
## Quick start
|
| 149 |
|
| 150 |
-
### Docker (most reliable
|
|
|
|
| 151 |
```bash
|
| 152 |
docker compose -f docker/docker-compose.yml up --build
|
| 153 |
# open http://localhost:8080
|
| 154 |
```
|
|
|
|
| 155 |
Brings up a 3-node demo cluster + dashboard on one machine.
|
| 156 |
|
| 157 |
### Python (real machines)
|
|
|
|
| 158 |
On every machine (`pip install -e .`):
|
|
|
|
| 159 |
```bash
|
| 160 |
export MASTER_ADDR=100.101.102.10 # coordinator IP (Tailscale 100.x recommended)
|
| 161 |
export MASTER_PORT=29560
|
|
@@ -166,192 +107,102 @@ daisychain-train
|
|
| 166 |
```
|
| 167 |
|
| 168 |
### Windows helper
|
|
|
|
| 169 |
```bat
|
| 170 |
scripts\setup.bat
|
| 171 |
```
|
|
|
|
| 172 |
An interactive menu: Docker, Python node, or just install deps.
|
| 173 |
|
| 174 |
Full walkthrough: **[docs/QUICKSTART.md](docs/QUICKSTART.md)**.
|
| 175 |
|
| 176 |
-
###
|
|
|
|
| 177 |
```bash
|
| 178 |
python -m daisychain.spikewhale_panel
|
| 179 |
# open http://localhost:8899
|
| 180 |
```
|
| 181 |
-
A web control panel: pick model size / training settings with sliders, choose any
|
| 182 |
-
HuggingFace dataset you have access to (default: streamed FineWeb-Edu), hit
|
| 183 |
-
Start, and watch the live loss. Stop and re-adjust any time with
|
| 184 |
-
**← Back to settings**. Launches the real DaisyChain training underneath.
|
| 185 |
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
| 187 |
```bash
|
| 188 |
cd web && npm install && node server.js
|
| 189 |
# open http://localhost:8787 on every device
|
| 190 |
```
|
| 191 |
-
|
| 192 |
-
(Snapdrop-style) and train a shared model **peer-to-peer over WebRTC**, computing
|
| 193 |
-
through the same verified INT8 units (WebGPU, with the identical units on CPU
|
| 194 |
-
for machines without it — there is no plain-float path). Private cross-network
|
| 195 |
-
rooms via `?room=CODE` with **host approval** — the room creator accepts each
|
| 196 |
-
device before it can join. Includes gradient averaging with a deterministic
|
| 197 |
-
Adam optimizer (identical state on every peer, nothing extra over the wire),
|
| 198 |
-
checkpoint **download** (.pt) and **upload → broadcast** so one device can
|
| 199 |
-
restore the whole group after a failure.
|
| 200 |
|
| 201 |
**Live demo:** https://huggingface.co/spaces/Quazim0t0/DaisyChain-Web
|
| 202 |
|
| 203 |
-
### Recent updates (July 2026)
|
| 204 |
-
|
| 205 |
-
**Verification stack**
|
| 206 |
-
|
| 207 |
-
- **IEEE-754 oracle** (`web/test_ieee.js`)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
- **
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
bugs no relation can see.
|
| 216 |
-
- **Exact kernel gates on every live kernel**, a continuous audit at live
|
| 217 |
-
shapes, and a cross-device kernel probe (same seeded int8 GEMM, same hash on
|
| 218 |
-
every honest device, any backend). The audit's sampling was rebuilt against
|
| 219 |
-
a named bug class: its old constants (6 cells, 2% of GEMMs) bounded an
|
| 220 |
-
overhead that had never been measured (auditing *every* GEMM costs <0.01% of
|
| 221 |
-
a step), and uniform random cells cannot see a last-row/column bug at a
|
| 222 |
-
16512-wide output. Sampling is now **stratified** — the first cells are the
|
| 223 |
-
structural danger points, chosen deliberately. On a last-column bug, same
|
| 224 |
-
cell budget: uniform caught **5/300** audits, stratified **300/300**, with
|
| 225 |
-
zero false positives.
|
| 226 |
-
- **RDNA2 ISA audit**: reading a real GPU's shader ISA against our
|
| 227 |
-
determinism assumptions confirmed three of them on silicon (exact packed
|
| 228 |
-
int8 dot; correctly-rounded f32 add/mul; 1-ULP reciprocal — division stays
|
| 229 |
-
off the GPU) and produced two hardenings. (1) Real ISAs have non-IEEE
|
| 230 |
-
variants that **flush −0 to +0**; JS `!==` can't see that (`-0 !== 0` is
|
| 231 |
-
false), so all gates and audits now compare **bit patterns** — exactly what
|
| 232 |
-
the replica hash sees. (2) FMA contraction of the quantize's `x·inv + 0.5`
|
| 233 |
-
(one rounding instead of two) turned out to be **floor-invisible by
|
| 234 |
-
construction** — proven in `test_b2b.js` with 175k+ last-ulp anomalies at
|
| 235 |
-
binade edges, zero surviving `floor()`. Rounding mode and denorm flushing
|
| 236 |
-
are runtime driver state on real hardware, which is why every device
|
| 237 |
-
re-runs the exact gates at every init.
|
| 238 |
-
|
| 239 |
-
**Training data** — FineWeb-Edu (10BT sample) is the hardcoded dataset. The
|
| 240 |
-
Space reads random slices of the parquet shards straight off the HF CDN with
|
| 241 |
-
range requests (pure-JS `hyparquet`, SNAPPY) and serves plain text at `/data`
|
| 242 |
-
— no dependency on the datasets-server rows API and its 503s.
|
| 243 |
-
|
| 244 |
-
**Resilience** — the sync guard now *repairs* instead of halting: a roster
|
| 245 |
-
gradient that reached the leader but not some follower (asymmetric WebRTC
|
| 246 |
-
mesh) is re-requested from the leader, bit-exact, and the run continues. The
|
| 247 |
-
guard still stops anything that would fork the weights.
|
| 248 |
|
| 249 |
**CUTLASS-style kernel work**, each step proven bit-identical or exact-gated:
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
- **
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
**
|
| 261 |
-
|
| 262 |
-
- **
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
~17 MB at the 16512-token vocab, uploaded *twice*. One upload, one encoder,
|
| 268 |
-
one submit: that pair went 205 → 90 ms and the step **12% faster**. The
|
| 269 |
-
fusion is gated bit-for-bit against the two calls it replaces.
|
| 270 |
-
- **A negative result, kept on purpose**: the remaining hot kernel looked
|
| 271 |
-
cache-hostile (adjacent lanes wrote 66 KB apart), but making the writes
|
| 272 |
-
contiguous changed nothing. Two probes explain why — holding the output at
|
| 273 |
-
17 MB while cutting compute 32× barely moved the time. The logits GEMM is
|
| 274 |
-
**transfer-bound, not compute-bound**, and the readback cannot be removed
|
| 275 |
-
because softmax must stay in JS (WGSL's `exp` is not correctly rounded, and
|
| 276 |
-
a per-vendor `exp` would fork replicas). The real lever there is the
|
| 277 |
-
vocabulary, not the kernel.
|
| 278 |
-
- **An init backend race was tried and removed**: it tied on the shipped path,
|
| 279 |
-
cost ~430 ms of init, and made the backend vary between page loads, which
|
| 280 |
-
silently invalidated three A/B comparisons before it was caught. A knob that
|
| 281 |
-
changes what you are measuring is worse than a fixed choice.
|
| 282 |
-
|
| 283 |
-
**Dirty-buffer gate — and the assumption it falsified.** Pooling introduced a
|
| 284 |
-
bug class the gates predate: a pooled buffer is *not* zero-initialized, so a
|
| 285 |
-
kernel that assumes zeros is right on step one and wrong on step two. That is
|
| 286 |
-
a **state** bug, where no single call is wrong and the *sequence* is, which is
|
| 287 |
-
the family no oracle can reach. The assumption was that the gates were blind
|
| 288 |
-
to it. Mutation-testing the gate proved otherwise: deleting the zeroing made
|
| 289 |
-
the plain gate fail at its *second* shape, because the sweep's own shapes
|
| 290 |
-
recycle each other's buffers. The suite had **incidental** coverage nobody
|
| 291 |
-
designed, which is coverage nobody can rely on — shorten the shape list and it
|
| 292 |
-
evaporates with the gate still green. It is now deliberate: the pool is
|
| 293 |
-
poisoned with 1e4-magnitude residue before a re-sweep, so detection no longer
|
| 294 |
-
depends on ordering luck. ~90 ms one-time.
|
| 295 |
-
|
| 296 |
-
**Scoring the oracles against my OWN bugs** (`web/test_selfcorpus.js`) — the
|
| 297 |
-
external corpus measures kernel bugs someone else wrote down, so this suite
|
| 298 |
-
asks the harder question: what do the instruments score against the four real
|
| 299 |
-
bugs of the month? Properties **0/2** on the data-plane pair (the `c·out`
|
| 300 |
-
theorem again), differential **2/2** — but half the bugs were not in the
|
| 301 |
-
kernels at all. A dead gate is a bug in a *checker*, caught only by mutating
|
| 302 |
-
the gate; a stalled roster gradient is a bug in the *protocol*, where every
|
| 303 |
-
computed value on every peer was correct, so no data oracle could fire. Those
|
| 304 |
-
needed different instruments, not better oracles.
|
| 305 |
-
|
| 306 |
-
All thirteen test suites (`cd web && npm test`) pass; results with methodology in
|
| 307 |
-
[`web/TEST_RESULTS.md`](web/TEST_RESULTS.md).
|
| 308 |
|
| 309 |
---
|
| 310 |
|
| 311 |
## How it works
|
| 312 |
|
| 313 |
-
Each machine runs the **same** command
|
| 314 |
-
model. Two things happen:
|
| 315 |
|
| 316 |
-
1. **The compute runs through the emulated GPU logic.** By default the model is
|
| 317 |
-
|
| 318 |
-
ReLU is done by the **bundled verified INT8 units** (`daisychain/verified/`) —
|
| 319 |
-
the emulated GPU math. Rank 0 prints **cluster-wide unit-invocation counts**
|
| 320 |
-
so you can see the emulated logic doing the work.
|
| 321 |
-
2. **The machines are pooled data-parallel.** Each node trains on its own shard;
|
| 322 |
-
gradients are capacity-weighted and combined into the exact full-batch
|
| 323 |
-
gradient, so replicas stay **bit-identical**. Faster machines automatically
|
| 324 |
-
take a bigger share.
|
| 325 |
|
| 326 |
```
|
| 327 |
-
old machine A
|
| 328 |
-
old machine B
|
| 329 |
-
old machine C
|
| 330 |
```
|
| 331 |
|
| 332 |
## Bring your own model
|
| 333 |
-
|
| 334 |
-
`examples/my_task_template.py`, set `DAISY_TASK=your_module:YourTask`. Use
|
| 335 |
-
`VerifiedLinear` (see `daisychain/verified_task.py`) to run your model's compute
|
| 336 |
-
through the emulated units. See **[docs/CUSTOM_TASK.md](docs/CUSTOM_TASK.md)**.
|
| 337 |
|
| 338 |
## Plain-float alternative
|
| 339 |
-
|
| 340 |
-
`DAISY_TASK=daisychain.example_task:ExampleTask`. Same cluster, same pooling
|
| 341 |
-
the model math just runs as ordinary float instead of through the verified units.
|
| 342 |
|
| 343 |
## The dashboard
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
(per-node device, weight, batch), and live training loss.
|
| 347 |
|
| 348 |
## Networking
|
| 349 |
-
|
| 350 |
-
on one interface
|
| 351 |
|
| 352 |
---
|
| 353 |
|
| 354 |
## Layout
|
|
|
|
| 355 |
```
|
| 356 |
daisychain/cluster.py capacity-weighted CPU/GPU data-parallel trainer
|
| 357 |
daisychain/train.py entry point (daisychain-train)
|
|
@@ -372,94 +223,52 @@ web/ DaisyChain-Web: P2P browser training (WebRTC + WebG
|
|
| 372 |
export_luts_web.py regenerates web/public LUTs from the trained units
|
| 373 |
```
|
| 374 |
|
| 375 |
-
## Recent updates (30 September 2026)
|
| 376 |
|
| 377 |
-
Six fixes, each found by applying a lesson from differential-testing an RDNA2 shader
|
| 378 |
-
translator: *a second implementation nobody checks is not verified*, *inactive or absent
|
| 379 |
-
participants must not leak into a reduction*, *a max over NaN is undefined*, and *know your
|
| 380 |
-
accumulator width*. Every one is covered by a check that fails on the old code.
|
| 381 |
-
`python test_verified_units.py` + `python test_cluster_reductions.py` and
|
| 382 |
-
`cd web && npm test` (thirteen suites) reproduce all of it.
|
| 383 |
|
| 384 |
| # | where | what was wrong | now |
|
| 385 |
| --- | --- | --- | --- |
|
| 386 |
-
| 1 | browser, WebGPU MLP chain | the row max comes from an `atomicMax` over `bitcast(abs(h))` and never passed the non-finite guard the CPU path has; a NaN/Inf `h1` became `inv = 0`/NaN and the GPU quantized `floor(NaN)` (implementation-defined) where a CPU peer threw | `scalesFromAbsMax` refuses a non-finite max. NaN and Inf bits order above every finite value as u32, so the row max is non-finite **iff** some element is
|
| 387 |
| 2 | browser, `averageGrads` | a gradient of a different length was silently averaged over a prefix (typed arrays ignore out-of-range writes); a NaN/Inf gradient went straight into DaisyAdam, poisoning `m`/`v` on **every** peer identically, so no fork check could see it | refuses both with `GradientError`; the training loop halts at that step with weights and Adam state untouched (still a valid checkpoint) |
|
| 388 |
-
| 3 | browser, int8 GEMMs (JS and WGSL) | the `Int32Array` and WGSL `i32` accumulators wrap past K = 131,071
|
| 389 |
-
| 4 | Python, logged cluster loss | `sum(loss_i) / world`
|
| 390 |
| 5 | Python, gradient all-reduce | one node's NaN was summed into every replica and applied | `check_finite_grads` refuses the reduced gradient before `opt.step()`; every rank holds the same sum, so every rank stops at the same step with no extra communication |
|
| 391 |
| 6 | Python, `pick_device` | with `CUDA_VISIBLE_DEVICES` set but **empty** (a common "CPU only"), torch reports `is_available() = True` and `device_count() = 0`, and the node crashed at startup | a device must actually exist; falls back to CPU |
|
| 392 |
|
| 393 |
-
Unchanged for valid inputs: finite gradients, finite activations, and K
|
| 394 |
-
bit-identical results to before, so updated and older peers still co-train. A 2-rank
|
| 395 |
-
end-to-end `DaisyCluster.fit` run stays bit-identical across replicas (`replica_diff 0.0`).
|
| 396 |
-
The WebGPU-side changes are host-side JavaScript (checked by `node --check` and by the
|
| 397 |
-
Node suites through the shared mirror functions); the WGSL kernels themselves are unchanged.
|
| 398 |
|
| 399 |
-
## Recent updates (September 2026)
|
| 400 |
|
| 401 |
-
Everything below is measured
|
| 402 |
-
`cd web && npm test` (twelve suites) reproduce it.
|
| 403 |
|
| 404 |
-
**The requant rounds instead of truncating.** `NeuralRequant16` was
|
| 405 |
-
`sat_int8(x >> 8)`, and Python's `>>` on negatives is an arithmetic floor, so every
|
| 406 |
-
requantized activation carried a systematic bias. Over the full int16 domain:
|
| 407 |
|
| 408 |
| requant | mean error | disagrees with the other on |
|
| 409 |
| --- | --- | --- |
|
| 410 |
| floor `x >> 8` (old) | **-0.4981 LSB** | 32,640 / 65,536 |
|
| 411 |
| round-half-up `(x + 128) >> 8` (now) | +0.0020 LSB | |
|
| 412 |
|
| 413 |
-
The shipped `requant16.pt` implemented the floor exactly, so this was real behaviour,
|
| 414 |
-
|
| 415 |
-
at
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
unit is a fork. Tie rule, stated precisely: this is half-UP; Python/numpy/torch
|
| 419 |
-
`round` is half-to-even. The two differ on exactly the 128 exact ties (0.2% of
|
| 420 |
-
inputs) and both are unbiased to within 0.002 LSB. The Python fleet quantizes with
|
| 421 |
-
`np.round` and the browser with `floor(x + 0.5)`; the two fleets never co-train, so
|
| 422 |
-
that cannot fork a group.
|
| 423 |
-
|
| 424 |
-
**Bounded elementwise units.** The GEMM backends already capped their temporaries;
|
| 425 |
-
the units right after them did not. `relu_array` / `requant_array` received the
|
| 426 |
-
whole activation matrix in the proof path at ~1.5–2 KB per element (262,144
|
| 427 |
-
elements = **407 MB**), and the multiply-LUT build pushed 262,144 rows through the
|
| 428 |
-
atom net at startup (**277 MB RSS on every node**). Both are now blocked:
|
| 429 |
-
407 MB → **2 MB, flat with N**; 277 MB → **1 MB**. Elementwise blocking is exact
|
| 430 |
-
(no accumulation order), verified bit-identical at N = 1 / 999 / 32768 / 32769 /
|
| 431 |
-
100000. `dataset()` builders are vectorised (65,536 Python iterations → one numpy
|
| 432 |
-
pass, 0.015 s), which is what makes retraining a unit practical at all.
|
| 433 |
-
|
| 434 |
-
**A NaN or Inf no longer quantizes to zeros.** A float → int8 conversion has no
|
| 435 |
-
answer for NaN/Inf, and both trainers gave a silent wrong one:
|
| 436 |
|
| 437 |
| path | input | old result |
|
| 438 |
| --- | --- | --- |
|
| 439 |
-
| Python `qat.py` | `[0.5, nan, 3.0]` | scale = NaN
|
| 440 |
-
| Python `qat.py` | `[0.5, inf, 3.0]` | scale = Inf
|
| 441 |
-
| browser quantizers | `[0.5, Inf, 3]` | scale = Infinity
|
| 442 |
| browser quantizers | `[0.5, NaN, 3]` | NaN skipped by the \|max\| scan, stored as **0** |
|
| 443 |
|
| 444 |
-
One bad value zeroed the whole layer's int8 input and training carried on. Now
|
| 445 |
-
`VerifiedLinear` raises `FloatingPointError`, and every browser quantizer
|
| 446 |
-
(`quantize`, `quantizeRows`, `quantizeCols`, `rowAbsMax`, `quantizeHeadCols`, and
|
| 447 |
-
the transformer's `quantizeColsAsRows`) throws `NonFiniteError`, naming how many
|
| 448 |
-
values were bad. Finite inputs are untouched, so builds with and without the guard
|
| 449 |
-
still co-train. Both new tests fail against the old code (Python: 2 checks; web:
|
| 450 |
-
`test_nonfinite.js`, 12) and pass now.
|
| 451 |
|
| 452 |
-
## Recent updates (August 2026)
|
| 453 |
|
| 454 |
-
Three changes to `daisychain/verified/`, each measured rather than asserted.
|
| 455 |
-
`python test_verified_units.py` (24 checks) reproduces all of it.
|
| 456 |
|
| 457 |
-
**Bounded GEMM memory.** `LUTBackend.gemm` and `NeuralBackend.gemm` materialized
|
| 458 |
-
the whole `(m, n, k)` product block before reducing it, so peak memory was cubic
|
| 459 |
-
in layer width. At the SpikeWhale panel's maximum (hidden 768, sequence 512) a
|
| 460 |
-
single layer's forward allocated **2.4 GB** — on the spare hardware this project
|
| 461 |
-
exists to use. Both now block the `m` axis. The contraction axis `k` is untouched,
|
| 462 |
-
so the sum and its order are unchanged and results stay bit-identical:
|
| 463 |
|
| 464 |
| GEMM | before | after |
|
| 465 |
| --- | --- | --- |
|
|
@@ -468,54 +277,38 @@ so the sum and its order are unchanged and results stay bit-identical:
|
|
| 468 |
| 512x768x768 (panel max) | 2426.9 MB | **77.2 MB** |
|
| 469 |
| neural backend, 64x96x96 | 519.2 MB | **57.5 MB** |
|
| 470 |
|
| 471 |
-
Caps are class attributes (`LUTBackend.max_block_bytes`,
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
The
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
**The multiply LUT is certified, not trusted.** `build_luts()` returned its tables
|
| 481 |
-
unchecked while the docs described a self-certify gate. Because the table *is* the
|
| 482 |
-
complete finite domain, certifying it **is** the exhaustive verification:
|
| 483 |
-
`LUTBackend.__init__` now checks all 65536 entries against signed integer multiply
|
| 484 |
-
and raises on mismatch. Cost: **0.5 ms**.
|
| 485 |
-
|
| 486 |
-
**Full-domain tests for the paths that ship.** The units' exhaustive verification
|
| 487 |
-
covers the scalar entry points (`NeuralMul8.verify()` walks `mul()`); production
|
| 488 |
-
runs the batched ones — `mul_array`, `relu_array`, `requant_array` — and the LUTs
|
| 489 |
-
built from them, which are different code. `test_verified_units.py` checks every
|
| 490 |
-
shipped path against golden integer arithmetic over its complete domain. All pass
|
| 491 |
-
today; nothing would have caught a regression before.
|
| 492 |
-
|
| 493 |
-
**Two guards against silent no-ops.** `gemm_int8` enforces the int32 accumulator
|
| 494 |
-
bound (`MAX_INT32_K = 131071`) instead of asserting it in a comment, since layer
|
| 495 |
-
width is user-selectable and an overflow would produce wrong numbers rather than
|
| 496 |
-
raise. `instrument.require(**minimums)` fails when a verified unit was
|
| 497 |
-
under-invoked, and refuses to report at all while counting is disabled — a zero
|
| 498 |
-
from a probe that was switched off is not evidence that the units did not run.
|
| 499 |
|
| 500 |
---
|
| 501 |
|
| 502 |
## Install
|
|
|
|
| 503 |
```bash
|
| 504 |
pip install torch numpy psutil
|
| 505 |
pip install -e . # exposes: daisychain-train, daisychain-agent, daisychain-dashboard
|
| 506 |
```
|
| 507 |
-
|
| 508 |
-
on **Windows use Docker/WSL** (see [Limitations](docs/LIMITS.md)).
|
| 509 |
|
| 510 |
---
|
| 511 |
|
| 512 |
## Links
|
|
|
|
| 513 |
- **DaisyChain on Hugging Face:** https://huggingface.co/DaisyChainAI
|
| 514 |
- **This model:** https://huggingface.co/DaisyChainAI/DaisyChain-Train
|
| 515 |
|
| 516 |
**License:** MIT · **Author:** Dean Byrne (Quazim0t0) · **Org:** DaisyChainAI
|
| 517 |
|
| 518 |
## Citation
|
|
|
|
| 519 |
```bibtex
|
| 520 |
@misc{byrne2026daisychain,
|
| 521 |
title = {DaisyChain-Train: An Old Hardware Training Pipeline},
|
|
|
|
| 8 |
- webrtc
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# DaisyChain-Train - old hardware training pipeline
|
| 12 |
|
| 13 |
+
Part of DaisyChain on Hugging Face: https://huggingface.co/DaisyChainAI
|
|
|
|
| 14 |
|
| 15 |
+
Model page (weights + card): https://huggingface.co/DaisyChainAI/DaisyChain-Train
|
| 16 |
|
| 17 |
+
I built this so spare / old machines can train a shared model. The training runs through emulated GPU logic: verified INT8 units (GUDA-style) that stand in for a GPU's math. Machines without a modern GPU still do the work. Chain several together and they train one model as a cluster.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
Before you rely on it, read what it cannot do: [Limitations](docs/LIMITS.md).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
Use the hardware you already have. Each machine runs the emulated GPU logic (verified INT8 units: multiply / requantize / ReLU). DaisyChain pools the machines data-parallel: device selection, capacity-weighted sharding, gradient sync, a P2P setup, and a live dashboard. Two ways to run: **Docker** or **Python**.
|
| 22 |
|
| 23 |
+
## Read this first
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
DaisyChain-Train is for **small models on spare hardware**. It **pools compute, not memory** (the model must fit on one machine). Scaling is **sublinear**. It is **not** a substitute for a real GPU on large models. Full envelope: **[docs/LIMITS.md](docs/LIMITS.md)**. Read that before you depend on it.
|
| 26 |
|
| 27 |
## Feature list
|
| 28 |
|
| 29 |
### Python cluster trainer (`daisychain/`)
|
| 30 |
+
|
| 31 |
+
- **Data-parallel training across mixed machines.** Each node trains its own shard. Gradients combine into the exact full-batch gradient. Replicas stay bit-identical.
|
| 32 |
+
- **Capacity-weighted sharding.** Faster machines automatically take a bigger share of the batch.
|
| 33 |
+
- **Emulated GPU compute (verified INT8 units).** `VerifiedLinear` layers run every forward multiply / requantize / ReLU through the bundled trained units. Rank 0 prints cluster-wide unit-invocation counts.
|
| 34 |
+
- **Bring your own model.** Any `Task` (`build_model` / `sample` / `loss`) via `DAISY_TASK`. Template in `examples/my_task_template.py`.
|
| 35 |
+
- **Plain-float alternative task.** Same cluster and pooling with ordinary float math.
|
| 36 |
+
- **Live dashboard** (`daisychain-dashboard`). Readiness banner, P2P connectivity scan, pooled cores/RAM, per-node capacity plan, live loss.
|
| 37 |
+
- **SpikeWhale control panel** (`spikewhale_panel`, `localhost:8899`). Sliders for model size / training settings, any HF dataset you can access (default streamed FineWeb-Edu), start/stop/re-adjust, live loss.
|
| 38 |
+
- **Docker demo cluster.** 3 nodes + dashboard in one command.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
- **Windows helper** (`scripts\setup.bat`) and Tailscale mesh guide.
|
| 40 |
|
| 41 |
+
### DaisyChain-Web (`web/`) - browser P2P training
|
| 42 |
+
|
| 43 |
+
- **Zero-install nodes.** Opening the page IS joining. Devices on one network auto-group (Snapdrop-style, by public IP).
|
| 44 |
+
- **Private cross-network rooms.** `?room=CODE` with **host approval** for every join.
|
| 45 |
+
- **Full WebRTC mesh.** Gradients travel peer-to-peer. The server only signals and serves static files. It never sees weights or gradients.
|
| 46 |
+
- **Leader-follower runs.** Whoever presses Start sets width / sequence / batch-per-device / steps / learning rate for the whole group. Config is broadcast on the wire.
|
| 47 |
+
- **Mid-run join.** Late devices are synced in (weights + step) and contribute from the next step.
|
| 48 |
+
- **Bit-identical replicas.** Same seeded init, strict roster-order gradient averaging, deterministic Adam with identical state on every peer. Verified live by per-step weight hashes.
|
| 49 |
+
- **Sync guard.** Any weight-hash mismatch stops the run instead of training past a fork. The step roster forbids silent partial averages.
|
| 50 |
+
- **Gradient repair.** A follower missing a roster gradient re-requests it from the leader (8 steps retained), bit-exact, and the run continues.
|
| 51 |
+
- **Cross-device kernel probe.** Every step, every device re-hashes a fixed seeded int8 GEMM through its live kernel. Catches broken arithmetic that weight hashes cannot see.
|
| 52 |
+
- **Hardcoded FineWeb-Edu streaming.** The server reads random slices of the 10BT parquet shards straight off the HF CDN via HTTP range requests (pure-JS `hyparquet`). Built-in corpus fallback offline.
|
| 53 |
+
- **Checkpoints.** Download `.pt`. Upload -> **broadcast to the whole group**. Validated (magic, dims, tokenizer vocab) before accepting.
|
| 54 |
+
- **Inference kit.** One self-contained HTML file with the trained weights baked in. Generations offline, anywhere.
|
| 55 |
+
- **In-page generation.** Prompt box on the trained model.
|
| 56 |
+
- **Old-hardware tier.** No WebGPU? The identical units run on CPU (same bits, so CPU and GPU devices co-train in one group). There is no plain-float path.
|
| 57 |
+
- **Large-message fragmentation.** Multi-MB gradients/checkpoints chunked at 48 KB over the data channels.
|
| 58 |
+
|
| 59 |
+
### Verified compute and kernels (web)
|
| 60 |
+
|
| 61 |
+
- **Verified INT8 units everywhere.** Block-scaled int8 GEMM: exact LUT products, exact int32 accumulation, bit-exact f32 epilogue with a pinned rounding schedule. Scales derived in JS f64 (division never runs on GPU).
|
| 62 |
+
- **Backends, best-first.** DP4A hardware int8 dot -> LUT compute shader -> CPU mirror. Every kernel is **exact-gated at init** (bit-level compares) and demoted to the mirror on any mismatch.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
- **Continuous random-cell audit** at live training shapes.
|
| 64 |
+
- **Fused attention kernels.** Gather/scatter head-strided q·kᵀ and a·v straight from BT×C layout (CUTLASS ex. 36/52 style).
|
| 65 |
+
- **QKV dual-GEMM fusion.** Shared left operand quantized once, one batch-3 dispatch (ex. 45). Bit-identical.
|
| 66 |
+
- **B2B MLP chain.** Both MLP GEMMs back-to-back on GPU with fused per-row absmax reduction and on-device quantize (ex. 13 + 23). WGSL-exact respec with a fround-stepped JS mirror. FMA-contraction-immune by construction.
|
| 67 |
+
- **Dispatch-optimized backward.** Overlapped independent GEMMs, batch-3 sibling fusions (ex. 05/24). Bit-identical gradients. Optional int8 STE backward path (dormant, 1.21x vs float).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
### Verification stack (web)
|
| 70 |
+
|
| 71 |
+
- **Exact init gates** on every kernel, every device, every boot. Including gates that "gate the gate" with discriminating boundary inputs.
|
| 72 |
+
- **IEEE-754 binary32 oracle** in exact BigInt arithmetic. Proves the JS epilogue mirror is spec-correct (rejects the old mirror on 34% of inputs).
|
| 73 |
+
- **Metamorphic property suite.** Reference-free relations + definitional absolutes. **4/4** on an externally-authored bug corpus, matching the differential gate.
|
| 74 |
+
- **RDNA2 ISA audit hardenings.** Bit-level (-0-aware) gate comparisons. Proof that FMA contraction cannot change the quantize.
|
| 75 |
+
- **Twelve-suite test chain** (`cd web && npm test`): convergence, replicas, oracle, gates, properties, external corpus, **self-corpus** (the instruments scored against my own bugs), B2B, optimizer, transformer LM, int8 backward, non-finite quantize refusal. Results in [web/TEST_RESULTS.md](web/TEST_RESULTS.md).
|
| 76 |
+
- **Dirty-buffer gate.** The pool is poisoned before a re-sweep so state bugs (a kernel assuming zeroed memory) are caught deterministically rather than by ordering luck.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
### Documentation
|
| 79 |
+
|
| 80 |
+
- Python: [QUICKSTART](docs/QUICKSTART.md), [LIMITS](docs/LIMITS.md), [CUSTOM_TASK](docs/CUSTOM_TASK.md), [TAILSCALE](docs/TAILSCALE.md).
|
| 81 |
+
- Web: [Getting started](web/docs/GETTING_STARTED.md), [Architecture](web/docs/ARCHITECTURE.md), [Verification](web/docs/VERIFICATION.md), [Troubleshooting](web/docs/TROUBLESHOOTING.md).
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
---
|
| 84 |
|
| 85 |
## Quick start
|
| 86 |
|
| 87 |
+
### Docker (most reliable - one command)
|
| 88 |
+
|
| 89 |
```bash
|
| 90 |
docker compose -f docker/docker-compose.yml up --build
|
| 91 |
# open http://localhost:8080
|
| 92 |
```
|
| 93 |
+
|
| 94 |
Brings up a 3-node demo cluster + dashboard on one machine.
|
| 95 |
|
| 96 |
### Python (real machines)
|
| 97 |
+
|
| 98 |
On every machine (`pip install -e .`):
|
| 99 |
+
|
| 100 |
```bash
|
| 101 |
export MASTER_ADDR=100.101.102.10 # coordinator IP (Tailscale 100.x recommended)
|
| 102 |
export MASTER_PORT=29560
|
|
|
|
| 107 |
```
|
| 108 |
|
| 109 |
### Windows helper
|
| 110 |
+
|
| 111 |
```bat
|
| 112 |
scripts\setup.bat
|
| 113 |
```
|
| 114 |
+
|
| 115 |
An interactive menu: Docker, Python node, or just install deps.
|
| 116 |
|
| 117 |
Full walkthrough: **[docs/QUICKSTART.md](docs/QUICKSTART.md)**.
|
| 118 |
|
| 119 |
+
### SpikeWhale control panel (sliders -> real training)
|
| 120 |
+
|
| 121 |
```bash
|
| 122 |
python -m daisychain.spikewhale_panel
|
| 123 |
# open http://localhost:8899
|
| 124 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
A web control panel: pick model size / training settings with sliders, choose any Hugging Face dataset you have access to (default: streamed FineWeb-Edu), hit Start, and watch the live loss. Stop and re-adjust any time with **<- Back to settings**. Launches the real DaisyChain training underneath.
|
| 127 |
+
|
| 128 |
+
### DaisyChain-Web (train by opening a browser tab)
|
| 129 |
+
|
| 130 |
```bash
|
| 131 |
cd web && npm install && node server.js
|
| 132 |
# open http://localhost:8787 on every device
|
| 133 |
```
|
| 134 |
+
|
| 135 |
+
Zero-install browser training. Devices on the same network auto-group (Snapdrop-style) and train a shared model **peer-to-peer over WebRTC**, computing through the same verified INT8 units (WebGPU, with the identical units on CPU for machines without it - there is no plain-float path). Private cross-network rooms via `?room=CODE` with **host approval**. The room creator accepts each device before it can join. Includes gradient averaging with a deterministic Adam optimizer (identical state on every peer, nothing extra over the wire), checkpoint **download** (`.pt`) and **upload -> broadcast** so one device can restore the whole group after a failure.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
**Live demo:** https://huggingface.co/spaces/Quazim0t0/DaisyChain-Web
|
| 138 |
|
| 139 |
+
### Recent updates (July 2026) - DaisyChain-Web
|
| 140 |
+
|
| 141 |
+
**Verification stack.** The browser trainer's correctness is now checked by things that run, not argued. Full results: [web/TEST_RESULTS.md](web/TEST_RESULTS.md).
|
| 142 |
+
|
| 143 |
+
- **IEEE-754 oracle** (`web/test_ieee.js`). A binary32 oracle built from the standard in exact BigInt arithmetic proves the JS epilogue mirror is spec-correct, and rejects the old round-once mirror on 34% of inputs.
|
| 144 |
+
- **Metamorphic properties + oracle mutation scoring** (`test_metamorphic.js`, `test_corpus.js`). Properties needing no reference implementation, scored against an externally-authored bug taxonomy. **4/4**, matching the exact differential gate's 4/4. Relations own the loop bugs. Two definitional absolutes (ReLU output range, a unit-scale integer anchor) own the value bugs no relation can see.
|
| 145 |
+
- **Exact kernel gates on every live kernel**, a continuous audit at live shapes, and a cross-device kernel probe (same seeded int8 GEMM, same hash on every honest device, any backend). The audit's sampling was rebuilt against a named bug class. Its old constants (6 cells, 2% of GEMMs) bounded an overhead that had never been measured (auditing *every* GEMM costs <0.01% of a step), and uniform random cells cannot see a last-row/column bug at a 16512-wide output. Sampling is now **stratified**: the first cells are the structural danger points, chosen deliberately. On a last-column bug, same cell budget: uniform caught **5/300** audits, stratified **300/300**, with zero false positives.
|
| 146 |
+
- **RDNA2 ISA audit.** Reading a real GPU's shader ISA against our determinism assumptions confirmed three of them on silicon (exact packed int8 dot; correctly-rounded f32 add/mul; 1-ULP reciprocal - division stays off the GPU) and produced two hardenings. (1) Real ISAs have non-IEEE variants that **flush -0 to +0**. JS `!==` cannot see that (`-0 !== 0` is false), so all gates and audits now compare **bit patterns** - exactly what the replica hash sees. (2) FMA contraction of the quantize's `x·inv + 0.5` (one rounding instead of two) turned out to be **floor-invisible by construction** - proven in `test_b2b.js` with 175k+ last-ulp anomalies at binade edges, zero surviving `floor()`. Rounding mode and denorm flushing are runtime driver state on real hardware, which is why every device re-runs the exact gates at every init.
|
| 147 |
+
|
| 148 |
+
**Training data.** FineWeb-Edu (10BT sample) is the hardcoded dataset. The Space reads random slices of the parquet shards straight off the HF CDN with range requests (pure-JS `hyparquet`, SNAPPY) and serves plain text at `/data`. No dependency on the datasets-server rows API and its 503s.
|
| 149 |
+
|
| 150 |
+
**Resilience.** The sync guard now *repairs* instead of halting. A roster gradient that reached the leader but not some follower (asymmetric WebRTC mesh) is re-requested from the leader, bit-exact, and the run continues. The guard still stops anything that would fork the weights.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
**CUTLASS-style kernel work**, each step proven bit-identical or exact-gated:
|
| 153 |
+
|
| 154 |
+
- **Dispatch-optimized backward** (ex. 05/24): independent GEMMs overlapped, sibling trios fused into batch-3 dispatches. Bit-identical gradients. Dormant int8-backward path down from 1.63x to 1.21x vs float.
|
| 155 |
+
- **QKV dual-GEMM fusion** (ex. 45): q/k/v share one left operand. Quantized once, one batched dispatch, zero changed bits.
|
| 156 |
+
- **B2B MLP chain** (ex. 13 + 23): both MLP GEMMs back-to-back on the GPU with a fused per-row absmax reduction. The intermediate is quantized on-device via a WGSL-exact respec (`floor(f32(x·invScale)+0.5)` - no GPU division) whose fround-stepped JS mirror keeps mixed GPU/CPU fleets bit-identical.
|
| 157 |
+
|
| 158 |
+
**Profile-driven speed work.** Every change below is bit-identical (gradient and loss hashes unchanged), so none of it trades correctness for wall clock:
|
| 159 |
+
|
| 160 |
+
- **Buffer pooling.** GPU buffers are recycled by size bucket instead of being created and destroyed per dispatch (~19 per MLP call, per layer, per step). **6-10% faster**, every hash unchanged.
|
| 161 |
+
- **Shared-operand embedding GEMMs.** Profiling put two f32 backward GEMMs at **55% of the entire step**, and both consumed the same `dlogits` operand: ~17 MB at the 16512-token vocab, uploaded *twice*. One upload, one encoder, one submit: that pair went 205 -> 90 ms and the step **12% faster**. The fusion is gated bit-for-bit against the two calls it replaces.
|
| 162 |
+
- **A negative result, kept on purpose.** The remaining hot kernel looked cache-hostile (adjacent lanes wrote 66 KB apart), but making the writes contiguous changed nothing. Two probes explain why: holding the output at 17 MB while cutting compute 32x barely moved the time. The logits GEMM is **transfer-bound, not compute-bound**, and the readback cannot be removed because softmax must stay in JS (WGSL's `exp` is not correctly rounded, and a per-vendor `exp` would fork replicas). The real lever there is the vocabulary, not the kernel.
|
| 163 |
+
- **An init backend race was tried and removed.** It tied on the shipped path, cost ~430 ms of init, and made the backend vary between page loads, which silently invalidated three A/B comparisons before it was caught. A knob that changes what you are measuring is worse than a fixed choice.
|
| 164 |
+
|
| 165 |
+
**Dirty-buffer gate - and the assumption it falsified.** Pooling introduced a bug class the gates predate: a pooled buffer is *not* zero-initialized, so a kernel that assumes zeros is right on step one and wrong on step two. That is a **state** bug, where no single call is wrong and the *sequence* is, which is the family no oracle can reach. The assumption was that the gates were blind to it. Mutation-testing the gate proved otherwise: deleting the zeroing made the plain gate fail at its *second* shape, because the sweep's own shapes recycle each other's buffers. The suite had **incidental** coverage nobody designed, which is coverage nobody can rely on. Shorten the shape list and it evaporates with the gate still green. It is now deliberate: the pool is poisoned with 1e4-magnitude residue before a re-sweep, so detection no longer depends on ordering luck. ~90 ms one-time.
|
| 166 |
+
|
| 167 |
+
**Scoring the oracles against my OWN bugs** (`web/test_selfcorpus.js`). The external corpus measures kernel bugs someone else wrote down, so this suite asks the harder question: what do the instruments score against the four real bugs of the month? Properties **0/2** on the data-plane pair (the `c·out` theorem again), differential **2/2** - but half the bugs were not in the kernels at all. A dead gate is a bug in a *checker*, caught only by mutating the gate. A stalled roster gradient is a bug in the *protocol*, where every computed value on every peer was correct, so no data oracle could fire. Those needed different instruments, not better oracles.
|
| 168 |
+
|
| 169 |
+
All thirteen test suites (`cd web && npm test`) pass. Results with methodology in [`web/TEST_RESULTS.md`](web/TEST_RESULTS.md).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
---
|
| 172 |
|
| 173 |
## How it works
|
| 174 |
|
| 175 |
+
Each machine runs the **same** command. They form a cluster and train one shared model. Two things happen:
|
|
|
|
| 176 |
|
| 177 |
+
1. **The compute runs through the emulated GPU logic.** By default the model is built from `VerifiedLinear` layers, so every forward multiply / requantize / ReLU is done by the **bundled verified INT8 units** (`daisychain/verified/`) - the emulated GPU math. Rank 0 prints **cluster-wide unit-invocation counts** so you can see the emulated logic doing the work.
|
| 178 |
+
2. **The machines are pooled data-parallel.** Each node trains on its own shard. Gradients are capacity-weighted and combined into the exact full-batch gradient, so replicas stay **bit-identical**. Faster machines automatically take a bigger share.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
```
|
| 181 |
+
old machine A -+
|
| 182 |
+
old machine B -+- each runs the emulated GPU logic on its shard -> one model
|
| 183 |
+
old machine C -+ (gradients combined across the cluster)
|
| 184 |
```
|
| 185 |
|
| 186 |
## Bring your own model
|
| 187 |
+
|
| 188 |
+
DaisyChain-Train trains any **Task** (`build_model` / `sample` / `loss`). Copy `examples/my_task_template.py`, set `DAISY_TASK=your_module:YourTask`. Use `VerifiedLinear` (see `daisychain/verified_task.py`) to run your model's compute through the emulated units. See **[docs/CUSTOM_TASK.md](docs/CUSTOM_TASK.md)**.
|
|
|
|
|
|
|
| 189 |
|
| 190 |
## Plain-float alternative
|
| 191 |
+
|
| 192 |
+
To skip the emulated units and train with normal float math on each machine, set `DAISY_TASK=daisychain.example_task:ExampleTask`. Same cluster, same pooling. The model math just runs as ordinary float instead of through the verified units.
|
|
|
|
| 193 |
|
| 194 |
## The dashboard
|
| 195 |
+
|
| 196 |
+
`daisychain-dashboard` (or the Docker service) serves a Tailwind page at `:8080`: readiness banner, P2P connectivity scan, pooled cores/RAM + capacity plan (per-node device, weight, batch), and live training loss.
|
|
|
|
| 197 |
|
| 198 |
## Networking
|
| 199 |
+
|
| 200 |
+
Use **Tailscale** for a P2P mesh so machines on different networks get stable IPs on one interface. **[docs/TAILSCALE.md](docs/TAILSCALE.md)**.
|
| 201 |
|
| 202 |
---
|
| 203 |
|
| 204 |
## Layout
|
| 205 |
+
|
| 206 |
```
|
| 207 |
daisychain/cluster.py capacity-weighted CPU/GPU data-parallel trainer
|
| 208 |
daisychain/train.py entry point (daisychain-train)
|
|
|
|
| 223 |
export_luts_web.py regenerates web/public LUTs from the trained units
|
| 224 |
```
|
| 225 |
|
| 226 |
+
## Recent updates (30 September 2026) - reductions refuse what they used to accept
|
| 227 |
|
| 228 |
+
Six fixes, each found by applying a lesson from differential-testing an RDNA2 shader translator: *a second implementation nobody checks is not verified*, *inactive or absent participants must not leak into a reduction*, *a max over NaN is undefined*, and *know your accumulator width*. Every one is covered by a check that fails on the old code. `python test_verified_units.py` + `python test_cluster_reductions.py` and `cd web && npm test` (thirteen suites) reproduce all of it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
| # | where | what was wrong | now |
|
| 231 |
| --- | --- | --- | --- |
|
| 232 |
+
| 1 | browser, WebGPU MLP chain | the row max comes from an `atomicMax` over `bitcast(abs(h))` and never passed the non-finite guard the CPU path has; a NaN/Inf `h1` became `inv = 0`/NaN and the GPU quantized `floor(NaN)` (implementation-defined) where a CPU peer threw | `scalesFromAbsMax` refuses a non-finite max. NaN and Inf bits order above every finite value as u32, so the row max is non-finite **iff** some element is - one check covers the whole row (checked on 2000 random rows) |
|
| 233 |
| 2 | browser, `averageGrads` | a gradient of a different length was silently averaged over a prefix (typed arrays ignore out-of-range writes); a NaN/Inf gradient went straight into DaisyAdam, poisoning `m`/`v` on **every** peer identically, so no fork check could see it | refuses both with `GradientError`; the training loop halts at that step with weights and Adam state untouched (still a valid checkpoint) |
|
| 234 |
+
| 3 | browser, int8 GEMMs (JS and WGSL) | the `Int32Array` and WGSL `i32` accumulators wrap past K = 131,071 - CPU and GPU agree, on a wrong sum | refused with `AccumulatorBoundError` at every GEMM entry point; the same bound as the Python kernel's `MAX_INT32_K` |
|
| 235 |
+
| 4 | Python, logged cluster loss | `sum(loss_i) / world` - an equal-weight mean of **unequal**, capacity-weighted batches | `cluster_mean_loss` = Σ wᵢ·lossᵢ with the gradient's own weights. Measured on 3- and 13-sample nodes: old **1.418**, true full-batch loss **0.959**, new 0.959 |
|
| 236 |
| 5 | Python, gradient all-reduce | one node's NaN was summed into every replica and applied | `check_finite_grads` refuses the reduced gradient before `opt.step()`; every rank holds the same sum, so every rank stops at the same step with no extra communication |
|
| 237 |
| 6 | Python, `pick_device` | with `CUDA_VISIBLE_DEVICES` set but **empty** (a common "CPU only"), torch reports `is_available() = True` and `device_count() = 0`, and the node crashed at startup | a device must actually exist; falls back to CPU |
|
| 238 |
|
| 239 |
+
Unchanged for valid inputs: finite gradients, finite activations, and K <= 131,071 produce bit-identical results to before, so updated and older peers still co-train. A 2-rank end-to-end `DaisyCluster.fit` run stays bit-identical across replicas (`replica_diff 0.0`). The WebGPU-side changes are host-side JavaScript (checked by `node --check` and by the Node suites through the shared mirror functions). The WGSL kernels themselves are unchanged.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
## Recent updates (September 2026) - requant rounding, bounded units, non-finite guards
|
| 242 |
|
| 243 |
+
Everything below is measured. `python test_verified_units.py` (25 checks) plus `cd web && npm test` (twelve suites) reproduce it.
|
|
|
|
| 244 |
|
| 245 |
+
**The requant rounds instead of truncating.** `NeuralRequant16` was `sat_int8(x >> 8)`, and Python's `>>` on negatives is an arithmetic floor, so every requantized activation carried a systematic bias. Over the full int16 domain:
|
|
|
|
|
|
|
| 246 |
|
| 247 |
| requant | mean error | disagrees with the other on |
|
| 248 |
| --- | --- | --- |
|
| 249 |
| floor `x >> 8` (old) | **-0.4981 LSB** | 32,640 / 65,536 |
|
| 250 |
| round-half-up `(x + 128) >> 8` (now) | +0.0020 LSB | |
|
| 251 |
|
| 252 |
+
The shipped `requant16.pt` implemented the floor exactly, so this was real behaviour, not a stale docstring. It was **retrained** against the new reference and saved only at 65,536/65,536 (a 99.9% unit would silently break the LUT equivalence the browser relies on), and `web/public/requant_lut.bin` was **re-exported** to match. The invariant is *neural forward == LUT == native integer op*, so a table that lags its unit is a fork. Tie rule, stated precisely: this is half-UP. Python/numpy/torch `round` is half-to-even. The two differ on exactly the 128 exact ties (0.2% of inputs) and both are unbiased to within 0.002 LSB. The Python fleet quantizes with `np.round` and the browser with `floor(x + 0.5)`. The two fleets never co-train, so that cannot fork a group.
|
| 253 |
+
|
| 254 |
+
**Bounded elementwise units.** The GEMM backends already capped their temporaries; the units right after them did not. `relu_array` / `requant_array` received the whole activation matrix in the proof path at ~1.5-2 KB per element (262,144 elements = **407 MB**), and the multiply-LUT build pushed 262,144 rows through the atom net at startup (**277 MB RSS on every node**). Both are now blocked: 407 MB -> **2 MB, flat with N**; 277 MB -> **1 MB**. Elementwise blocking is exact (no accumulation order), verified bit-identical at N = 1 / 999 / 32768 / 32769 / 100000. `dataset()` builders are vectorised (65,536 Python iterations -> one numpy pass, 0.015 s), which is what makes retraining a unit practical at all.
|
| 255 |
+
|
| 256 |
+
**A NaN or Inf no longer quantizes to zeros.** A float -> int8 conversion has no answer for NaN/Inf, and both trainers gave a silent wrong one:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
| path | input | old result |
|
| 259 |
| --- | --- | --- |
|
| 260 |
+
| Python `qat.py` | `[0.5, nan, 3.0]` | scale = NaN -> **`[0, 0, 0]`** (only a RuntimeWarning) |
|
| 261 |
+
| Python `qat.py` | `[0.5, inf, 3.0]` | scale = Inf -> **`[0, 0, 0]`** |
|
| 262 |
+
| browser quantizers | `[0.5, Inf, 3]` | scale = Infinity -> **`[0, 0, 0]`** |
|
| 263 |
| browser quantizers | `[0.5, NaN, 3]` | NaN skipped by the \|max\| scan, stored as **0** |
|
| 264 |
|
| 265 |
+
One bad value zeroed the whole layer's int8 input and training carried on. Now `VerifiedLinear` raises `FloatingPointError`, and every browser quantizer (`quantize`, `quantizeRows`, `quantizeCols`, `rowAbsMax`, `quantizeHeadCols`, and the transformer's `quantizeColsAsRows`) throws `NonFiniteError`, naming how many values were bad. Finite inputs are untouched, so builds with and without the guard still co-train. Both new tests fail against the old code (Python: 2 checks; web: `test_nonfinite.js`, 12) and pass now.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
+
## Recent updates (August 2026) - verified compute path
|
| 268 |
|
| 269 |
+
Three changes to `daisychain/verified/`, each measured rather than asserted. `python test_verified_units.py` (24 checks) reproduces all of it.
|
|
|
|
| 270 |
|
| 271 |
+
**Bounded GEMM memory.** `LUTBackend.gemm` and `NeuralBackend.gemm` materialized the whole `(m, n, k)` product block before reducing it, so peak memory was cubic in layer width. At the SpikeWhale panel's maximum (hidden 768, sequence 512) a single layer's forward allocated **2.4 GB** - on the spare hardware this project exists to use. Both now block the `m` axis. The contraction axis `k` is untouched, so the sum and its order are unchanged and results stay bit-identical:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
| GEMM | before | after |
|
| 274 |
| --- | --- | --- |
|
|
|
|
| 277 |
| 512x768x768 (panel max) | 2426.9 MB | **77.2 MB** |
|
| 278 |
| neural backend, 64x96x96 | 519.2 MB | **57.5 MB** |
|
| 279 |
|
| 280 |
+
Caps are class attributes (`LUTBackend.max_block_bytes`, `NeuralBackend.max_products`), tunable per instance. Below the cap the LUT path takes the original single-block branch unchanged.
|
| 281 |
+
|
| 282 |
+
The two backends need *different* accounting, which is worth knowing before tuning them: a LUT product costs 8 bytes, but a neural product is split into four nibble pairs through a 128-wide net and costs ~880 bytes. Applying the LUT's byte budget to the neural path computes a block 110x too large and never splits at all.
|
| 283 |
+
|
| 284 |
+
**The multiply LUT is certified, not trusted.** `build_luts()` returned its tables unchecked while the docs described a self-certify gate. Because the table *is* the complete finite domain, certifying it **is** the exhaustive verification: `LUTBackend.__init__` now checks all 65536 entries against signed integer multiply and raises on mismatch. Cost: **0.5 ms**.
|
| 285 |
+
|
| 286 |
+
**Full-domain tests for the paths that ship.** The units' exhaustive verification covers the scalar entry points (`NeuralMul8.verify()` walks `mul()`). Production runs the batched ones: `mul_array`, `relu_array`, `requant_array`, and the LUTs built from them, which are different code. `test_verified_units.py` checks every shipped path against golden integer arithmetic over its complete domain. All pass today. Nothing would have caught a regression before.
|
| 287 |
+
|
| 288 |
+
**Two guards against silent no-ops.** `gemm_int8` enforces the int32 accumulator bound (`MAX_INT32_K = 131071`) instead of asserting it in a comment, since layer width is user-selectable and an overflow would produce wrong numbers rather than raise. `instrument.require(**minimums)` fails when a verified unit was under-invoked, and refuses to report at all while counting is disabled. A zero from a probe that was switched off is not evidence that the units did not run.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
---
|
| 291 |
|
| 292 |
## Install
|
| 293 |
+
|
| 294 |
```bash
|
| 295 |
pip install torch numpy psutil
|
| 296 |
pip install -e . # exposes: daisychain-train, daisychain-agent, daisychain-dashboard
|
| 297 |
```
|
| 298 |
+
|
| 299 |
+
Requires Python >= 3.9, PyTorch >= 2.0. Multi-node is reliable on **Linux/macOS**. On **Windows use Docker/WSL** (see [Limitations](docs/LIMITS.md)).
|
| 300 |
|
| 301 |
---
|
| 302 |
|
| 303 |
## Links
|
| 304 |
+
|
| 305 |
- **DaisyChain on Hugging Face:** https://huggingface.co/DaisyChainAI
|
| 306 |
- **This model:** https://huggingface.co/DaisyChainAI/DaisyChain-Train
|
| 307 |
|
| 308 |
**License:** MIT · **Author:** Dean Byrne (Quazim0t0) · **Org:** DaisyChainAI
|
| 309 |
|
| 310 |
## Citation
|
| 311 |
+
|
| 312 |
```bibtex
|
| 313 |
@misc{byrne2026daisychain,
|
| 314 |
title = {DaisyChain-Train: An Old Hardware Training Pipeline},
|