Quazim0t0 commited on
Commit
07789b3
·
verified ·
1 Parent(s): 4060984

Rewrite model card in first person; ASCII hyphens only; keep all facts

Browse files
Files changed (1) hide show
  1. README.md +147 -354
README.md CHANGED
@@ -8,154 +8,95 @@ tags:
8
  - webrtc
9
  ---
10
 
11
- # 🌼 DaisyChain-Train — Old Hardware Training Pipeline
12
 
13
- **Part of DaisyChain on 🤗 Hugging Face → https://huggingface.co/DaisyChainAI**
14
- Model page (weights + card): https://huggingface.co/DaisyChainAI/DaisyChain-Train
15
 
16
- ---
17
 
18
- > **In plain terms:** DaisyChain-Train lets you use **old / spare machines** to
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
- **Use the hardware you already have to train.** Each machine runs the emulated
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
- ## ⚠️ Read this first
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
- - **Data-parallel training across mixed machines** — each node trains its own
45
- shard; gradients combine into the exact full-batch gradient, replicas stay
46
- bit-identical.
47
- - **Capacity-weighted sharding** — faster machines automatically take a
48
- bigger share of the batch.
49
- - **Emulated GPU compute (verified INT8 units)** — `VerifiedLinear` layers
50
- run every forward multiply / requantize / ReLU through the bundled trained
51
- units; cluster-wide unit-invocation counts printed by rank 0.
52
- - **Bring your own model** — any `Task` (`build_model` / `sample` / `loss`)
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/`) — browser P2P training
65
- - **Zero-install nodes** — opening the page IS joining; devices on one
66
- network auto-group (Snapdrop-style, by public IP).
67
- - **Private cross-network rooms** — `?room=CODE` with **host approval** for
68
- every join.
69
- - **Full WebRTC mesh** — gradients travel peer-to-peer; the server only
70
- signals and serves static files, it never sees weights or gradients.
71
- - **Leader-follower runs** — whoever presses Start sets width / sequence /
72
- batch-per-device / steps / learning rate for the whole group; config
73
- broadcast on the wire.
74
- - **Mid-run join** — late devices are synced in (weights + step) and
75
- contribute from the next step.
76
- - **Bit-identical replicas** — same seeded init, strict roster-order gradient
77
- averaging, deterministic Adam with identical state on every peer; verified
78
- live by per-step weight hashes.
79
- - **Sync guard** — any weight-hash mismatch stops the run instead of
80
- training past a fork; the step roster forbids silent partial averages.
81
- - **Gradient repair** — a follower missing a roster gradient re-requests it
82
- from the leader (8 steps retained), bit-exact, and the run continues.
83
- - **Cross-device kernel probe** — every step, every device re-hashes a fixed
84
- seeded int8 GEMM through its live kernel; catches broken arithmetic that
85
- weight hashes cannot see.
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** — gather/scatter head-strided q·kᵀ and a·v
109
- straight from BT×C layout (CUTLASS ex. 36/52 style).
110
- - **QKV dual-GEMM fusion** — shared left operand quantized once, one batch-3
111
- dispatch (ex. 45); bit-identical.
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
- - **Exact init gates** on every kernel, every device, every boot — including
121
- gates that "gate the gate" with discriminating boundary inputs.
122
- - **IEEE-754 binary32 oracle** in exact BigInt arithmetic — proves the JS
123
- epilogue mirror is spec-correct (rejects the old mirror on 34% of inputs).
124
- - **Metamorphic property suite** — reference-free relations + definitional
125
- absolutes; **4/4** on an externally-authored bug corpus, matching the
126
- differential gate.
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
- - Python: [QUICKSTART](docs/QUICKSTART.md), [LIMITS](docs/LIMITS.md),
140
- [CUSTOM_TASK](docs/CUSTOM_TASK.md), [TAILSCALE](docs/TAILSCALE.md).
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 — one command)
 
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
- ### 🐋 SpikeWhale control panel (sliders → real training)
 
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
- ### 🌐 DaisyChain-Web (train by opening a browser tab)
 
 
 
187
  ```bash
188
  cd web && npm install && node server.js
189
  # open http://localhost:8787 on every device
190
  ```
191
- Zero-install browser training: devices on the same network auto-group
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) — DaisyChain-Web
204
-
205
- **Verification stack** — the browser trainer's correctness is now *checked by
206
- things that run*, not argued ([full results](web/TEST_RESULTS.md)):
207
- - **IEEE-754 oracle** (`web/test_ieee.js`): a binary32 oracle built from the
208
- standard in exact BigInt arithmetic proves the JS epilogue mirror is
209
- spec-correct — and rejects the old round-once mirror on 34% of inputs.
210
- - **Metamorphic properties + oracle mutation scoring** (`test_metamorphic.js`,
211
- `test_corpus.js`): properties needing no reference implementation, scored
212
- against an externally-authored bug taxonomy — **4/4**, matching the exact
213
- differential gate's 4/4. Relations own the loop bugs; two definitional
214
- absolutes (ReLU output range, a unit-scale integer anchor) own the value
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
- - **Dispatch-optimized backward** (ex. 05/24): independent GEMMs overlapped,
251
- sibling trios fused into batch-3 dispatches — bit-identical gradients,
252
- dormant int8-backward path down from 1.63× to 1.21× vs float.
253
- - **QKV dual-GEMM fusion** (ex. 45): q/k/v share one left operand — quantized
254
- once, one batched dispatch, zero changed bits.
255
- - **B2B MLP chain** (ex. 13 + 23): both MLP GEMMs back-to-back on the GPU with
256
- a fused per-row absmax reduction; the intermediate is quantized on-device
257
- via a WGSL-exact respec (`floor(f32(x·invScale)+0.5)` — no GPU division)
258
- whose fround-stepped JS mirror keeps mixed GPU/CPU fleets bit-identical.
259
-
260
- **Profile-driven speed work** — every change below is bit-identical (gradient
261
- and loss hashes unchanged), so none of it trades correctness for wall clock:
262
- - **Buffer pooling**: GPU buffers are recycled by size bucket instead of being
263
- created and destroyed per dispatch (~19 per MLP call, per layer, per step).
264
- **6–10% faster**, every hash unchanged.
265
- - **Shared-operand embedding GEMMs**: profiling put two f32 backward GEMMs at
266
- **55% of the entire step**, and both consumed the same `dlogits` operand —
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; they form a cluster and train one shared
314
- model. Two things happen:
315
 
316
- 1. **The compute runs through the emulated GPU logic.** By default the model is
317
- built from `VerifiedLinear` layers, so every forward multiply / requantize /
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 ─┼─► each runs the emulated GPU logic on its shard ─► one model
329
- old machine C ─┘ (gradients combined across the cluster)
330
  ```
331
 
332
  ## Bring your own model
333
- DaisyChain-Train trains any **Task** (`build_model` / `sample` / `loss`). Copy
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
- To skip the emulated units and train with normal float math on each machine, set
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
- `daisychain-dashboard` (or the Docker service) serves a Tailwind page at `:8080`
345
- — readiness banner, P2P connectivity scan, pooled cores/RAM + capacity plan
346
- (per-node device, weight, batch), and live training loss.
347
 
348
  ## Networking
349
- Use **Tailscale** for a P2P mesh so machines on different networks get stable IPs
350
- on one interface — **[docs/TAILSCALE.md](docs/TAILSCALE.md)**.
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) — reductions refuse what they used to accept
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 — one check covers the whole row (checked on 2000 random rows) |
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 — 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` |
389
- | 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 |
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 ≤ 131,071 produce
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) — requant rounding, bounded units, non-finite guards
400
 
401
- Everything below is measured, and `python test_verified_units.py` (25 checks) plus
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
- not a stale docstring. It was **retrained** against the new reference and saved only
415
- at 65,536/65,536 (a 99.9% unit would silently break the LUT equivalence the browser
416
- relies on), and `web/public/requant_lut.bin` was **re-exported** to match: the
417
- invariant is *neural forward == LUT == native integer op*, so a table that lags its
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 → **`[0, 0, 0]`** (only a RuntimeWarning) |
440
- | Python `qat.py` | `[0.5, inf, 3.0]` | scale = Inf → **`[0, 0, 0]`** |
441
- | browser quantizers | `[0.5, Inf, 3]` | scale = Infinity → **`[0, 0, 0]`** |
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) — verified compute path
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
- `NeuralBackend.max_products`), tunable per instance. Below the cap the LUT path
473
- takes the original single-block branch unchanged.
474
-
475
- The two backends need *different* accounting, which is worth knowing before tuning
476
- them: a LUT product costs 8 bytes, but a neural product is split into four nibble
477
- pairs through a 128-wide net and costs ~880 bytes. Applying the LUT's byte budget
478
- to the neural path computes a block 110x too large and never splits at all.
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
- Requires Python ≥ 3.9, PyTorch ≥ 2.0. Multi-node is reliable on **Linux/macOS**;
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},