Add the 2026-10-23 parameter shard, ring masks, and README notes.
Browse filesA stage with two or more layers keeps one flat copy and unpacks at most two layers. A sliding window is a softmax mask. JSON and the position seed change only the pick. DeepGEMM's 128-channel scale stays out of the GEMM. The four docs list what was added and what was left out.
- GUIDE.md +28 -0
- README.md +75 -4
- docs/ARCHITECTURE.md +11 -0
- docs/GETTING_STARTED.md +8 -1
- public/app.js +42 -10
- public/arch.js +9 -0
- public/index.html +6 -0
- public/infer.js +99 -14
- public/shard.js +196 -1
- test_pipeline.js +149 -0
GUIDE.md
CHANGED
|
@@ -26,6 +26,14 @@ The rest of this file is the previous guide. This list, and every later note mar
|
|
| 26 |
- One weight range stays in flight while the previous body is widened to f32.
|
| 27 |
- A dropped peer or a stalled lap is replayed once. Weights still come from the Hub.
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
```bash
|
| 30 |
npm install
|
| 31 |
npm start # http://localhost:8788
|
|
@@ -78,6 +86,25 @@ The compute units are the live DaisyChain-Train browser cores (Hub revision `a28
|
|
| 78 |
- One weight range stays in flight while the previous body is widened to f32.
|
| 79 |
- A dropped peer or a stalled lap is replayed **once** from the start. Weights still come from the Hub.
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
## The ring
|
| 82 |
|
| 83 |
```
|
|
@@ -178,6 +205,7 @@ Without those, a test where both sides call the same code proves nothing.
|
|
| 178 |
*(added 2026-10-22)* The same file also checks a dropped probe, a refused non-finite activation,
|
| 179 |
a chunked prefill across a split, and a lookahead draft across a split. The
|
| 180 |
chunked path is compared to itself, not to the full-window GEMM.
|
|
|
|
| 181 |
|
| 182 |
This was also confirmed against a real model: SmolLM-135M's 30 layers split
|
| 183 |
`[10,10,10]`, `[1,14,15]`, `[0,15,15]` and `[5,5,5,5,5,5]` all produced token
|
|
|
|
| 26 |
- One weight range stays in flight while the previous body is widened to f32.
|
| 27 |
- A dropped peer or a stalled lap is replayed once. Weights still come from the Hub.
|
| 28 |
|
| 29 |
+
## Added 2026-10-23
|
| 30 |
+
|
| 31 |
+
- A stage with two or more layers packs those weights into one flat buffer and unpacks at most two layers during a forward. Embeddings and `lm_head` stay put. Weights do not cross the ring.
|
| 32 |
+
- Optimizer states, reduced gradients, a peer parameter all-gather, and tiled re-quantization are not implemented.
|
| 33 |
+
- From DeepInfer: sliding-window softmax mask, sampling tied to the token position, a JSON mask on the top 40 completion tokens, and tokenizing the next queued prompt during the current turn.
|
| 34 |
+
- Not from DeepInfer: CUDA kernels, expert parallelism, a confidence-head draft model, compact KV replay, disaggregated prefill.
|
| 35 |
+
- DeepGEMM's FP8 and FP4 kernels are not used. A scale every 128 channels of K is a different number from the one INT8 scale per row and per column.
|
| 36 |
+
|
| 37 |
```bash
|
| 38 |
npm install
|
| 39 |
npm start # http://localhost:8788
|
|
|
|
| 86 |
- One weight range stays in flight while the previous body is widened to f32.
|
| 87 |
- A dropped peer or a stalled lap is replayed **once** from the start. Weights still come from the Hub.
|
| 88 |
|
| 89 |
+
## Parameter shard *(added 2026-10-23)*
|
| 90 |
+
|
| 91 |
+
ZeRO stages 1 and 2 do not apply here. There is no optimizer and no backward. Parameters are not gathered from other devices.
|
| 92 |
+
|
| 93 |
+
When a stage owns two or more layers, those weights sit in one flat buffer. A forward unpacks the current layer and the next one, runs the layer, then drops the one just used. At most **2** layers are unpacked. A single layer stays resident. The embedding and `lm_head` are not in the shard. The bytes the GEMM reads are the same bytes, so a full-window split stays bit-identical. The flat buffer does not reduce the stage below one copy of its parameters.
|
| 94 |
+
|
| 95 |
+
## DeepInfer *(added 2026-10-23)*
|
| 96 |
+
|
| 97 |
+
DeepInfer's CUDA kernels, expert parallelism, and KV cluster are not used.
|
| 98 |
+
|
| 99 |
+
- A model with `use_sliding_window` and a positive `sliding_window` masks attention outside that window on layers at or after `max_window_layers`. The GEMM still includes every key, so the int8 scales do not change. Compact replay, which drops old keys, is not done.
|
| 100 |
+
- Above temperature 0, each token position has its own draw from the seed.
|
| 101 |
+
- Output set to JSON keeps the completion inside a JSON value, using the top **40** tokens. The hidden state is already finished before the mask.
|
| 102 |
+
- The next queued prompt is tokenized while the current turn runs. One turn still owns the ring.
|
| 103 |
+
|
| 104 |
+
## DeepGEMM *(added 2026-10-23)*
|
| 105 |
+
|
| 106 |
+
DeepGEMM's CUDA GEMMs, grouped expert GEMMs, and indexer are not used. The verified GEMM keeps one INT8 scale for a whole activation row and one for a whole weight column. A 128-channel scale would change the products. K above **131071** is still refused.
|
| 107 |
+
|
| 108 |
## The ring
|
| 109 |
|
| 110 |
```
|
|
|
|
| 205 |
*(added 2026-10-22)* The same file also checks a dropped probe, a refused non-finite activation,
|
| 206 |
a chunked prefill across a split, and a lookahead draft across a split. The
|
| 207 |
chunked path is compared to itself, not to the full-window GEMM.
|
| 208 |
+
*(added 2026-10-23)* The same file checks that a flat parameter shard matches a resident forward, on the full window and on one chunk, and that a third live layer is refused. It also checks a sliding-window mask across a split, a JSON mask, a position seed, a prepared prompt, and that a 128-channel weight scale differs from the verified column scale.
|
| 209 |
|
| 210 |
This was also confirmed against a real model: SmolLM-135M's 30 layers split
|
| 211 |
`[10,10,10]`, `[1,14,15]`, `[0,15,15]` and `[5,5,5,5,5,5]` all produced token
|
README.md
CHANGED
|
@@ -31,6 +31,14 @@ The rest of this file is the previous README. Everything in this list, and every
|
|
| 31 |
- One weight range stays in flight while the previous body is widened to f32.
|
| 32 |
- A planned peer other than the head dropping, or a stalled lap, replays the prompt once. Weights still come from the Hub.
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
---
|
| 35 |
|
| 36 |
> **In plain terms:** point it at a Hugging Face model, open the page on two or
|
|
@@ -45,7 +53,7 @@ machines:
|
|
| 45 |
```bash
|
| 46 |
npm install
|
| 47 |
npm start # http://localhost:8788
|
| 48 |
-
npm test #
|
| 49 |
```
|
| 50 |
|
| 51 |
Full walkthrough: **[docs/GETTING_STARTED.md](docs/GETTING_STARTED.md)** ·
|
|
@@ -98,6 +106,60 @@ On top of that, the head does a few things the old loop did not:
|
|
| 98 |
- Weight fetches keep one HTTP range in flight while the previous body is widened to f32. The bytes are the same.
|
| 99 |
- If a peer disconnects or a lap stalls, the head drops that peer, replans, and reruns the prompt **once** from the start. Weights still come from the Hub, not from another device.
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
## The ring
|
| 102 |
|
| 103 |
```
|
|
@@ -198,7 +260,13 @@ Without those, a test where both sides call the same code proves nothing.
|
|
| 198 |
*(added 2026-10-22)* The same file checks that a disagreeing probe is dropped, that a non-finite
|
| 199 |
activation is refused, that a chunked prefill matches across a split, and that
|
| 200 |
a lookahead draft matches across a split. The chunked path is compared to
|
| 201 |
-
itself, not to the full-window GEMM.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
Confirmed on a real model too: SmolLM-135M's 30 layers split `[10,10,10]`,
|
| 204 |
`[1,14,15]`, `[0,15,15]` and `[5,5,5,5,5,5]` all produced token sequences
|
|
@@ -236,8 +304,8 @@ public/safetensors.js header parsing, exact BF16/F16 widening, range coalesc
|
|
| 236 |
public/hf.js Hub client: range reads, gated repos, token redaction
|
| 237 |
public/arch.js config.json -> normalized spec; per-family tensor names
|
| 238 |
public/tokenizer.js byte-level BPE from the repo's own tokenizer.json
|
| 239 |
-
public/shard.js the plan, probe filter, lap estimate, prefix index, drafts (added 2026-10-22)
|
| 240 |
-
public/infer.js forward pass, chunked prefill, lookahead readout (added 2026-10-22)
|
| 241 |
public/wire.js binary protocol + routing, as pure functions
|
| 242 |
public/app.js WebRTC mesh, assignments, queue, replay, token loop (added 2026-10-22)
|
| 243 |
public/verified_core.js the verified INT8 units (copied from live DaisyChain-Train, 2026-10-22)
|
|
@@ -262,6 +330,9 @@ docs/ GETTING_STARTED, ARCHITECTURE
|
|
| 262 |
4 bytes per parameter it holds.
|
| 263 |
- **The head is a single point of failure.** *(added 2026-10-22.)* A stage that drops is removed and the
|
| 264 |
prompt is replayed once. If that also fails, press Generate again.
|
|
|
|
|
|
|
|
|
|
| 265 |
- **No authentication of activations.** A malicious stage that runs correct math
|
| 266 |
but returns a crafted activation is not caught by any of this. Peers see each
|
| 267 |
other's IPs, and the head sees your prompt. Ring with people you trust.
|
|
|
|
| 31 |
- One weight range stays in flight while the previous body is widened to f32.
|
| 32 |
- A planned peer other than the head dropping, or a stalled lap, replays the prompt once. Weights still come from the Hub.
|
| 33 |
|
| 34 |
+
## Added 2026-10-23
|
| 35 |
+
|
| 36 |
+
- ZeRO and FSDP, forward only, inside one stage. A stage with two or more layers packs those weights into one flat buffer and unpacks at most two layers for the forward. Embeddings and `lm_head` stay as they are. Weights still come from the Hub and do not cross the ring.
|
| 37 |
+
- ZeRO stages 1 and 2 are not implemented. Infer has no optimizer and no backward. Parameters are not all-gathered across peers. `TiledLinear` is not used.
|
| 38 |
+
- From DeepInfer: a sliding-window mask on layers that configure one, sampling tied to the token position, a JSON mask on the top 40 completion tokens, and tokenizing the next queued prompt while the current turn still runs.
|
| 39 |
+
- Not taken from DeepInfer: CUDA kernels, expert parallelism, a draft-model confidence head, compact KV replay, disaggregated prefill, and a KV store.
|
| 40 |
+
- DeepGEMM is not a second GEMM. Its FP8 and FP4 kernels, 128-channel scales, grouped MoE GEMM, and indexer stay out. The verified path keeps one INT8 scale per activation row and one per weight column.
|
| 41 |
+
|
| 42 |
---
|
| 43 |
|
| 44 |
> **In plain terms:** point it at a Hugging Face model, open the page on two or
|
|
|
|
| 53 |
```bash
|
| 54 |
npm install
|
| 55 |
npm start # http://localhost:8788
|
| 56 |
+
npm test # 59 checks: pipeline equivalence, wire protocol, loader
|
| 57 |
```
|
| 58 |
|
| 59 |
Full walkthrough: **[docs/GETTING_STARTED.md](docs/GETTING_STARTED.md)** ·
|
|
|
|
| 106 |
- Weight fetches keep one HTTP range in flight while the previous body is widened to f32. The bytes are the same.
|
| 107 |
- If a peer disconnects or a lap stalls, the head drops that peer, replans, and reruns the prompt **once** from the start. Weights still come from the Hub, not from another device.
|
| 108 |
|
| 109 |
+
## Parameter shard *(added 2026-10-23)*
|
| 110 |
+
|
| 111 |
+
ZeRO and FSDP split training state across data-parallel ranks. This project does not train, so the parts that move optimizer moments or gradients are left out.
|
| 112 |
+
|
| 113 |
+
What a stage does when it owns two or more layers:
|
| 114 |
+
|
| 115 |
+
- Those layers' float32 weights are copied once into one flat buffer. The per-layer arrays are then dropped. The embedding, the final norm, and `lm_head` stay where they are. `lm_head` stays on the head.
|
| 116 |
+
- A forward gathers the current layer as views into that buffer, prefetches the next layer, runs the same layer, then releases the one just used. At most **2** layers are unpacked.
|
| 117 |
+
- The views alias the flat buffer. The GEMM reads them. The full-window path stays bit-identical to keeping every layer resident. The chunked path uses the same gather.
|
| 118 |
+
- A stage with one layer keeps that layer resident.
|
| 119 |
+
- Peak RAM is still one copy of the stage's parameters. The flat buffer does not make the slice smaller.
|
| 120 |
+
- Weights are not sent to another device. Each device still fetches its own slice from the Hub.
|
| 121 |
+
|
| 122 |
+
Left out on purpose:
|
| 123 |
+
|
| 124 |
+
- ZeRO stage 1 (optimizer states) and stage 2 (reduced gradients). There is no Adam state and no backward.
|
| 125 |
+
- A parameter all-gather across the ring. That would move weights on every token.
|
| 126 |
+
- `TiledLinear`. Re-quantizing a matrix per tile would change the int8 scales.
|
| 127 |
+
- NVMe offload. The flat buffer is the CPU-resident copy.
|
| 128 |
+
|
| 129 |
+
## DeepInfer *(added 2026-10-23)*
|
| 130 |
+
|
| 131 |
+
[DeepInfer](https://github.com/deepseek-ai/DeepInfer) serves DeepSeek-V4.1-Flash on NVIDIA SM100 and Ascend. Most of that stack does not fit a browser ring that only runs Llama-style and GPT-2-style blocks.
|
| 132 |
+
|
| 133 |
+
Taken:
|
| 134 |
+
|
| 135 |
+
- **Sliding-window mask.** If `use_sliding_window` is set and `sliding_window` is positive, layers with index `>= max_window_layers` drop attention outside that window. The GEMM still sees every key column, so the int8 scales stay the scales of the full matrix. A window wider than the cache matches full causal attention. `use_sliding_window: false` leaves the mask off.
|
| 136 |
+
- **Position seed.** Above temperature 0, the draw for token position `p` depends on the seed and `p`. Scoring a longer draft does not move a later draw.
|
| 137 |
+
- **JSON mask.** The Output control can require the completion to stay inside a JSON value. The head checks the top **40** tokens after the hidden state is already computed. No legal token in that set falls back to the unconstrained pick. This does not change the hop.
|
| 138 |
+
- **Queued prompt.** The next prompt is tokenized while the current turn still holds the ring. The forward stays one turn at a time.
|
| 139 |
+
|
| 140 |
+
Left out:
|
| 141 |
+
|
| 142 |
+
- FlashMLA, DeepGEMM, DeepEP, TileKernels, and the other CUDA kernels. The verified INT8 units stay.
|
| 143 |
+
- Attention data parallel, context parallel, MoE expert parallel, and expert load balancing.
|
| 144 |
+
- DSpark's confidence head and next-n draft model. The prompt-lookup draft stays capped at **4** and is scored in the same window. Shortening that draft would not shrink the GEMM.
|
| 145 |
+
- Compact SWA replay (the V4.1 keep of **140**, or a **128**-token recompute). Dropping key rows would change int8 scales. Prefix hits still recompute.
|
| 146 |
+
- Disaggregated prefill and decode, the 3FS KV store, FP4 weights, and the multi-protocol recipe tokenizer.
|
| 147 |
+
|
| 148 |
+
## DeepGEMM *(added 2026-10-23)*
|
| 149 |
+
|
| 150 |
+
[DeepGEMM](https://github.com/deepseek-ai/DeepGEMM) is a CUDA kernel library for SM90 and SM100. It compiles FP8, FP4, and BF16 GEMMs at runtime, plus grouped MoE GEMMs, a fused Mega MoE, and the V3.2 indexer. None of those kernels run in this browser ring.
|
| 151 |
+
|
| 152 |
+
The numeric recipe is the part that would matter, and it does not match. DeepGEMM's FP8 GEMM uses one scale per **128** channels of K (`BLOCK_K == 128`). A spike in a later block does not widen the scale of an earlier block. `vgemmBlock` uses one INT8 scale for the whole activation row and one for the whole weight column. Swapping in the 128-channel scale would change every product, including the split against the unsplit run. The test `a 128-channel weight scale is not the verified column scale` pins that difference. The forward path does not call a block scale.
|
| 153 |
+
|
| 154 |
+
Also left out:
|
| 155 |
+
|
| 156 |
+
- Grouped GEMMs, masked grouped GEMMs, and the K-grouped backward. This ring has no experts and no backward.
|
| 157 |
+
- Mega MoE, NVLink overlap, and the lightning-indexer ReLU logits.
|
| 158 |
+
- UE8M0 packed scales, TMA layout, programmatic dependent launch, and JIT CUDA.
|
| 159 |
+
- A non-deterministic fast GEMM. The CPU mirror and the GPU kernel stay on the same INT8 units. K above **131071** is refused so the int32 accumulator does not wrap.
|
| 160 |
+
|
| 161 |
+
Weights are already stored `k x n` once at load. That is the layout a non-transposed A times a transposed B expects. It is not a new kernel.
|
| 162 |
+
|
| 163 |
## The ring
|
| 164 |
|
| 165 |
```
|
|
|
|
| 260 |
*(added 2026-10-22)* The same file checks that a disagreeing probe is dropped, that a non-finite
|
| 261 |
activation is refused, that a chunked prefill matches across a split, and that
|
| 262 |
a lookahead draft matches across a split. The chunked path is compared to
|
| 263 |
+
itself, not to the full-window GEMM. *(added 2026-10-23)* It also checks that a
|
| 264 |
+
flat parameter shard matches a resident forward on both the full-window path
|
| 265 |
+
and a chunk, that the flat bytes are unchanged, and that a third live layer
|
| 266 |
+
is refused. *(added 2026-10-23)* It also checks a sliding-window mask across a
|
| 267 |
+
split, a JSON mask, a position seed, and a prepared prompt id list. A
|
| 268 |
+
128-channel weight scale is checked to be a different number from the
|
| 269 |
+
verified whole-column scale.
|
| 270 |
|
| 271 |
Confirmed on a real model too: SmolLM-135M's 30 layers split `[10,10,10]`,
|
| 272 |
`[1,14,15]`, `[0,15,15]` and `[5,5,5,5,5,5]` all produced token sequences
|
|
|
|
| 304 |
public/hf.js Hub client: range reads, gated repos, token redaction
|
| 305 |
public/arch.js config.json -> normalized spec; per-family tensor names
|
| 306 |
public/tokenizer.js byte-level BPE from the repo's own tokenizer.json
|
| 307 |
+
public/shard.js the plan, probe filter, lap estimate, prefix index, drafts (added 2026-10-22); flat parameter shard (added 2026-10-23)
|
| 308 |
+
public/infer.js forward pass, chunked prefill, lookahead readout (added 2026-10-22); gather window of 2 (added 2026-10-23)
|
| 309 |
public/wire.js binary protocol + routing, as pure functions
|
| 310 |
public/app.js WebRTC mesh, assignments, queue, replay, token loop (added 2026-10-22)
|
| 311 |
public/verified_core.js the verified INT8 units (copied from live DaisyChain-Train, 2026-10-22)
|
|
|
|
| 330 |
4 bytes per parameter it holds.
|
| 331 |
- **The head is a single point of failure.** *(added 2026-10-22.)* A stage that drops is removed and the
|
| 332 |
prompt is replayed once. If that also fails, press Generate again.
|
| 333 |
+
- **No optimizer shard, and no weight gather across peers.** *(added 2026-10-23.)* A stage with two or more layers keeps one flat copy and unpacks at most two layers. The slice is not smaller than one copy of its parameters. Weights still come from the Hub.
|
| 334 |
+
- **No DeepInfer kernel stack.** *(added 2026-10-23.)* No expert parallel, no draft-model head, no compact KV drop, no disaggregated prefill. A sliding window is a softmax mask. JSON is a pick mask.
|
| 335 |
+
- **No DeepGEMM kernel.** *(added 2026-10-23.)* FP8, FP4, 128-channel scales, and grouped MoE GEMMs are not used. One INT8 scale covers each activation row and each weight column.
|
| 336 |
- **No authentication of activations.** A malicious stage that runs correct math
|
| 337 |
but returns a crafted activation is not caught by any of this. Peers see each
|
| 338 |
other's IPs, and the head sees your prompt. Ring with people you trust.
|
docs/ARCHITECTURE.md
CHANGED
|
@@ -15,6 +15,15 @@ The rest of this file is the previous architecture note. This list is what chang
|
|
| 15 |
- Prompts longer than 32 tokens on more than one stage use even chunks of about 16, two in flight, with per-stage KV. Not the full-window GEMM.
|
| 16 |
- A second prompt is queued. A dropped planned peer or a stalled lap replays the prompt once. A non-finite hidden state is not forwarded.
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
## The pieces
|
| 19 |
|
| 20 |
| File | Role |
|
|
@@ -110,6 +119,8 @@ credentials. Weights never travel between peers, which means:
|
|
| 110 |
rather than nine per layer;
|
| 111 |
- *(added 2026-10-22)* the next range is requested while the previous body is widened to f32.
|
| 112 |
The bytes do not change.
|
|
|
|
|
|
|
| 113 |
|
| 114 |
## One token
|
| 115 |
|
|
|
|
| 15 |
- Prompts longer than 32 tokens on more than one stage use even chunks of about 16, two in flight, with per-stage KV. Not the full-window GEMM.
|
| 16 |
- A second prompt is queued. A dropped planned peer or a stalled lap replays the prompt once. A non-finite hidden state is not forwarded.
|
| 17 |
|
| 18 |
+
## Added 2026-10-23
|
| 19 |
+
|
| 20 |
+
- `makeParamShard` packs a stage's layers into one flat Float32 buffer when the stage owns two or more. `runLayers` and `runLayersChunk` gather the current layer, prefetch the next, and release the one just used. The limit is 2 live layers.
|
| 21 |
+
- Head tensors stay outside that buffer. A one-layer stage is not sharded. No optimizer state is stored. No parameter is sent to a peer.
|
| 22 |
+
- A configured sliding window is a softmax mask. Key columns stay in the GEMM. `swaFrom` is `max_window_layers`.
|
| 23 |
+
- `seqDraw(seed, pos)` samples one position. `pickStructured` applies a JSON mask to the top 40 logits on the head. `preparedIds` is the prompt tokenized during the previous turn.
|
| 24 |
+
- DeepInfer kernels, expert parallel, the DSpark confidence head, compact KV truncation, and prefill/decode disaggregation are not implemented.
|
| 25 |
+
- DeepGEMM is not linked. `quantizeCols` keeps one scale per weight column for the whole K. A 128-channel scale is a different recipe and is not called from `vgemmBlock`. `MAX_INT32_K` is 131071.
|
| 26 |
+
|
| 27 |
## The pieces
|
| 28 |
|
| 29 |
| File | Role |
|
|
|
|
| 119 |
rather than nine per layer;
|
| 120 |
- *(added 2026-10-22)* the next range is requested while the previous body is widened to f32.
|
| 121 |
The bytes do not change.
|
| 122 |
+
- *(added 2026-10-23)* after the slice is arranged, a stage with two or more layers copies those layer weights into one flat buffer and drops the per-layer arrays. Embeddings, the final norm, and `lm_head` stay as separate arrays. A forward gathers at most two layers as views into the flat buffer (`forward_prefetch`, `limit_all_gathers` of 2, reshard after the layer). The views alias the buffer, so the GEMM sees the same bytes. ZeRO stage 1, ZeRO stage 2, a cross-peer all-gather, and tiled re-quantization are not done.
|
| 123 |
+
- *(added 2026-10-23)* `layerForward` reads `ly.swa`. A positive window limits the softmax, not the score GEMM. The next queued prompt is tokenized during the wait. JSON masking happens in `sampleAt` after logits exist.
|
| 124 |
|
| 125 |
## One token
|
| 126 |
|
docs/GETTING_STARTED.md
CHANGED
|
@@ -11,6 +11,12 @@ The rest of this file is the previous getting-started note. This list is what ch
|
|
| 11 |
- New log lines: lap estimate for the whole plan, queued prompts, overlap prefill, draft span. A disagreeing probe is left out of the plan.
|
| 12 |
- New failures: "a peer left the ring" (one replay), "REFUSED activation" for a non-finite hop, "refusing the run" when the lap estimate exceeds 120 seconds, "stays out of the plan: kernel probe".
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
## Run the server
|
| 15 |
|
| 16 |
```bash
|
|
@@ -119,7 +125,8 @@ few layers and no vocabulary.
|
|
| 119 |
- **overlap prefill** — *(added 2026-10-22)* chunks of about 16, two in flight, per-stage KV. Not the
|
| 120 |
full-window GEMM. Verify repeats this path.
|
| 121 |
- **draft span** — *(added 2026-10-22)* a repeated suffix scored in one lap on the full-window path.
|
| 122 |
-
- **my slice** — which layers this device holds and how much memory they take.
|
|
|
|
| 123 |
|
| 124 |
## Verify
|
| 125 |
|
|
|
|
| 11 |
- New log lines: lap estimate for the whole plan, queued prompts, overlap prefill, draft span. A disagreeing probe is left out of the plan.
|
| 12 |
- New failures: "a peer left the ring" (one replay), "REFUSED activation" for a non-finite hop, "refusing the run" when the lap estimate exceeds 120 seconds, "stays out of the plan: kernel probe".
|
| 13 |
|
| 14 |
+
## Added 2026-10-23
|
| 15 |
+
|
| 16 |
+
- A device that owns two or more layers shows `flat shard, 2 layers live` on its slice line. The weights are one buffer on that device. They are not fetched from a peer. A device with one layer still says `resident`.
|
| 17 |
+
- Output can be `free` or `JSON`. JSON masks the completion. The seed above temperature 0 is tied to the token position. A prompt typed while a turn is running is tokenized before that turn ends.
|
| 18 |
+
- DeepGEMM is not used. The multiply stays the verified INT8 GEMM, one scale per row and one per column.
|
| 19 |
+
|
| 20 |
## Run the server
|
| 21 |
|
| 22 |
```bash
|
|
|
|
| 125 |
- **overlap prefill** — *(added 2026-10-22)* chunks of about 16, two in flight, per-stage KV. Not the
|
| 126 |
full-window GEMM. Verify repeats this path.
|
| 127 |
- **draft span** — *(added 2026-10-22)* a repeated suffix scored in one lap on the full-window path.
|
| 128 |
+
- **my slice** — which layers this device holds and how much memory they take. *(added 2026-10-23)* Two or more layers are kept in one flat shard, with at most two unpacked during a forward. That does not shrink the download.
|
| 129 |
+
- **Output** — *(added 2026-10-23)* `free` or `JSON`. JSON applies after the hop, to the top 40 tokens of the completion.
|
| 130 |
|
| 131 |
## Verify
|
| 132 |
|
public/app.js
CHANGED
|
@@ -18,7 +18,7 @@ const ui = {};
|
|
| 18 |
for (const id of ["status", "backend", "me", "peers", "log", "requests", "roomInfo", "roomCode",
|
| 19 |
"copyLink", "model", "tokenizer", "plan", "myShard", "prompt", "genBtn",
|
| 20 |
"stopBtn", "out", "tps", "tokcount", "hops", "temp", "vtemp", "ntok", "vntok",
|
| 21 |
-
"seed", "verify", "verifyRow", "ringState", "repo", "revision", "loadRepo",
|
| 22 |
"modelState", "ctxLen", "vctxLen", "tokenState", "clearToken",
|
| 23 |
"tokModal", "tokInput", "tokOk", "tokCancel", "tokWhy",
|
| 24 |
"tokPasteRow", "tokSignIn", "tokSignInBtn",
|
|
@@ -636,7 +636,8 @@ async function loadMyStage(assign, st) {
|
|
| 636 |
stageObj = Infer.makeStage(s, st, w, kernelCtx(), assign.ctx || ctxLen());
|
| 637 |
stageObj.pass = assign.pass === "chunk" ? "chunk" : "window";
|
| 638 |
spec = s;
|
| 639 |
-
ui.myShard.textContent = `${st.head ? "head + " : ""}layers ${st.lo}–${st.hi - 1} · ${mb.toFixed(0)} MB
|
|
|
|
| 640 |
return stageObj;
|
| 641 |
}
|
| 642 |
|
|
@@ -764,11 +765,25 @@ async function onAct(peerId, buf) {
|
|
| 764 |
}
|
| 765 |
|
| 766 |
const promptQueue = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
|
| 768 |
async function generate(isReplay) {
|
| 769 |
if (running) {
|
| 770 |
const waiting = (ui.prompt.value || "").slice(0, 80);
|
| 771 |
promptQueue.push(ui.prompt.value);
|
|
|
|
| 772 |
log(`ring busy — queued (${promptQueue.length} waiting): "${waiting}". The turn already generating keeps the ring.`);
|
| 773 |
return;
|
| 774 |
}
|
|
@@ -782,8 +797,11 @@ async function generate(isReplay) {
|
|
| 782 |
plan = buildPlan(); modelHash = repoInfo.fingerprint; spec = repoInfo.spec;
|
| 783 |
renderPlan();
|
| 784 |
const promptText = ui.prompt.value || "The";
|
| 785 |
-
|
|
|
|
|
|
|
| 786 |
if (!idsPreview.length) idsPreview = [0];
|
|
|
|
| 787 |
const fits = Shard.headCanHold(spec, repoInfo.tensors, navigator.deviceMemory || 0);
|
| 788 |
const local = Shard.preferLocal(idsPreview.length, plan.length - 1, fits);
|
| 789 |
const chatKey = String((prefixIndex.blocks(idsPreview)[0]) || "empty");
|
|
@@ -815,6 +833,7 @@ async function generate(isReplay) {
|
|
| 815 |
pass: willChunk ? "chunk" : "window",
|
| 816 |
}));
|
| 817 |
log("waiting for every stage to fetch its layers…");
|
|
|
|
| 818 |
const ok = await waitFor(() => readyStages.size >= plan.length - 1, 600000);
|
| 819 |
if (!ok) { log("not every stage reported ready — generating anyway will stall, so stopping here."); throw new Error("stages not ready"); }
|
| 820 |
} else {
|
|
@@ -826,7 +845,12 @@ async function generate(isReplay) {
|
|
| 826 |
|
| 827 |
const seq = ++runSeq;
|
| 828 |
const nTok = +ui.ntok.value, temp = +ui.temp.value / 100;
|
| 829 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
let ids = idsPreview.slice();
|
| 831 |
prefixIndex.note(chatKey, ids);
|
| 832 |
// The bytes kept here are the prefix-block hashes, not layer KV. Reusing
|
|
@@ -859,6 +883,7 @@ async function generate(isReplay) {
|
|
| 859 |
await sendTo(plan[1].id, packAct(seq, index, plan[1].index, hidden));
|
| 860 |
hops += plan.length;
|
| 861 |
const key = `${seq}:${index}`;
|
|
|
|
| 862 |
const ok = await waitFor(() => actIn.has(key) || peerDropped, 60000);
|
| 863 |
if (peerDropped) throw new Error("peer dropped");
|
| 864 |
if (!ok) throw new Error("stalled");
|
|
@@ -882,7 +907,8 @@ async function generate(isReplay) {
|
|
| 882 |
let produced = 0;
|
| 883 |
while (produced < nTok && !abortRun && last) {
|
| 884 |
const row = (last.length / spec.hidden) - 1;
|
| 885 |
-
const id = Infer.
|
|
|
|
| 886 |
ids.push(id); produced++;
|
| 887 |
ui.out.textContent = Tokenizer.decode(tok, ids);
|
| 888 |
ui.tokcount.textContent = String(produced);
|
|
@@ -909,13 +935,16 @@ async function generate(isReplay) {
|
|
| 909 |
await sendTo(plan[1].id, packAct(seq, produced, plan[1].index, x));
|
| 910 |
hops += plan.length;
|
| 911 |
const key = `${seq}:${produced}`;
|
|
|
|
| 912 |
const ok = await waitFor(() => actIn.has(key) || peerDropped, 60000);
|
| 913 |
if (peerDropped) throw new Error("peer dropped");
|
| 914 |
if (!ok) throw new Error("stalled");
|
| 915 |
x = actIn.get(key); actIn.delete(key);
|
| 916 |
}
|
| 917 |
const rows = Infer.draftRows(T, frame.shown, draft.length);
|
| 918 |
-
const
|
|
|
|
|
|
|
| 919 |
const got = Shard.acceptDraft(picked, draft);
|
| 920 |
if (draft.length) log(`draft span ${draft.length}, accepted ${Math.max(0, got.length - 1)} before the bonus token. One lap.`);
|
| 921 |
for (const id of got) {
|
|
@@ -940,11 +969,14 @@ async function generate(isReplay) {
|
|
| 940 |
const all = { id: myId, index: 0, lo: 0, hi: spec.layers, head: true };
|
| 941 |
const solo = await loadMyStage({ repo: repoInfo.repo, revision: repoInfo.revision, spec, ctx: T }, all);
|
| 942 |
const promptIds = Tokenizer.encode(tok, ui.prompt.value || "The");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
const ref = useChunks
|
| 944 |
-
? await Infer.generateChunked([solo], promptIds.length ? promptIds : [0], nTok, 16,
|
| 945 |
-
|
| 946 |
-
: await Infer.generateLookahead([solo], promptIds.length ? promptIds : [0], nTok, T,
|
| 947 |
-
{ temperature: temp, topK: 40, rng: Infer.mulberry32(+ui.seed.value | 0), maxDraft: 4 });
|
| 948 |
const same = ref.length === ids.length && ref.every((v, i) => v === ids[i]);
|
| 949 |
log(same ? "VERIFIED: the distributed run and the single-device run produced identical token ids."
|
| 950 |
: `MISMATCH: the ring and this device disagree. Distributed "${Tokenizer.decode(tok, ids).slice(0, 60)}" ` +
|
|
|
|
| 18 |
for (const id of ["status", "backend", "me", "peers", "log", "requests", "roomInfo", "roomCode",
|
| 19 |
"copyLink", "model", "tokenizer", "plan", "myShard", "prompt", "genBtn",
|
| 20 |
"stopBtn", "out", "tps", "tokcount", "hops", "temp", "vtemp", "ntok", "vntok",
|
| 21 |
+
"seed", "schema", "verify", "verifyRow", "ringState", "repo", "revision", "loadRepo",
|
| 22 |
"modelState", "ctxLen", "vctxLen", "tokenState", "clearToken",
|
| 23 |
"tokModal", "tokInput", "tokOk", "tokCancel", "tokWhy",
|
| 24 |
"tokPasteRow", "tokSignIn", "tokSignInBtn",
|
|
|
|
| 636 |
stageObj = Infer.makeStage(s, st, w, kernelCtx(), assign.ctx || ctxLen());
|
| 637 |
stageObj.pass = assign.pass === "chunk" ? "chunk" : "window";
|
| 638 |
spec = s;
|
| 639 |
+
ui.myShard.textContent = `${st.head ? "head + " : ""}layers ${st.lo}–${st.hi - 1} · ${mb.toFixed(0)} MB` +
|
| 640 |
+
(stageObj.paramShard ? " flat shard, 2 layers live" : " resident");
|
| 641 |
return stageObj;
|
| 642 |
}
|
| 643 |
|
|
|
|
| 765 |
}
|
| 766 |
|
| 767 |
const promptQueue = [];
|
| 768 |
+
let scheduledPrompt = null;
|
| 769 |
+
|
| 770 |
+
// Tokenize the next queued prompt while this turn still holds the ring.
|
| 771 |
+
// The forward itself stays serial. Weights are not touched here.
|
| 772 |
+
function scheduleQueued() {
|
| 773 |
+
if (!tok || !promptQueue.length) return;
|
| 774 |
+
const text = promptQueue[0];
|
| 775 |
+
if (!text) return;
|
| 776 |
+
if (scheduledPrompt && scheduledPrompt.text === text) return;
|
| 777 |
+
let ids = [...Tokenizer.encode(tok, text)];
|
| 778 |
+
if (!ids.length) ids = [0];
|
| 779 |
+
scheduledPrompt = { text, ids };
|
| 780 |
+
}
|
| 781 |
|
| 782 |
async function generate(isReplay) {
|
| 783 |
if (running) {
|
| 784 |
const waiting = (ui.prompt.value || "").slice(0, 80);
|
| 785 |
promptQueue.push(ui.prompt.value);
|
| 786 |
+
scheduleQueued();
|
| 787 |
log(`ring busy — queued (${promptQueue.length} waiting): "${waiting}". The turn already generating keeps the ring.`);
|
| 788 |
return;
|
| 789 |
}
|
|
|
|
| 797 |
plan = buildPlan(); modelHash = repoInfo.fingerprint; spec = repoInfo.spec;
|
| 798 |
renderPlan();
|
| 799 |
const promptText = ui.prompt.value || "The";
|
| 800 |
+
const prepared = Shard.preparedIds(promptText, scheduledPrompt);
|
| 801 |
+
scheduledPrompt = null;
|
| 802 |
+
let idsPreview = prepared ? prepared : [...Tokenizer.encode(tok, promptText)];
|
| 803 |
if (!idsPreview.length) idsPreview = [0];
|
| 804 |
+
if (prepared) log("prompt was scheduled while the previous turn held the ring.");
|
| 805 |
const fits = Shard.headCanHold(spec, repoInfo.tensors, navigator.deviceMemory || 0);
|
| 806 |
const local = Shard.preferLocal(idsPreview.length, plan.length - 1, fits);
|
| 807 |
const chatKey = String((prefixIndex.blocks(idsPreview)[0]) || "empty");
|
|
|
|
| 833 |
pass: willChunk ? "chunk" : "window",
|
| 834 |
}));
|
| 835 |
log("waiting for every stage to fetch its layers…");
|
| 836 |
+
scheduleQueued();
|
| 837 |
const ok = await waitFor(() => readyStages.size >= plan.length - 1, 600000);
|
| 838 |
if (!ok) { log("not every stage reported ready — generating anyway will stall, so stopping here."); throw new Error("stages not ready"); }
|
| 839 |
} else {
|
|
|
|
| 845 |
|
| 846 |
const seq = ++runSeq;
|
| 847 |
const nTok = +ui.ntok.value, temp = +ui.temp.value / 100;
|
| 848 |
+
const schema = ui.schema && ui.schema.value === "json" ? "json" : "";
|
| 849 |
+
const opts = {
|
| 850 |
+
temperature: temp, topK: 40, seqSeed: +ui.seed.value | 0, schema,
|
| 851 |
+
decodeSeq: (arr) => Tokenizer.decode(tok, arr), promptLen: idsPreview.length,
|
| 852 |
+
};
|
| 853 |
+
if (schema === "json") log("JSON mask on the completion. Top 40 tokens. The hidden state is unchanged.");
|
| 854 |
let ids = idsPreview.slice();
|
| 855 |
prefixIndex.note(chatKey, ids);
|
| 856 |
// The bytes kept here are the prefix-block hashes, not layer KV. Reusing
|
|
|
|
| 883 |
await sendTo(plan[1].id, packAct(seq, index, plan[1].index, hidden));
|
| 884 |
hops += plan.length;
|
| 885 |
const key = `${seq}:${index}`;
|
| 886 |
+
scheduleQueued();
|
| 887 |
const ok = await waitFor(() => actIn.has(key) || peerDropped, 60000);
|
| 888 |
if (peerDropped) throw new Error("peer dropped");
|
| 889 |
if (!ok) throw new Error("stalled");
|
|
|
|
| 907 |
let produced = 0;
|
| 908 |
while (produced < nTok && !abortRun && last) {
|
| 909 |
const row = (last.length / spec.hidden) - 1;
|
| 910 |
+
const id = Infer.sampleAt((await Infer.logitsAt(stageObj, last, [row]))[0], opts, ids.length,
|
| 911 |
+
opts.decodeSeq, ids.slice(promptLen));
|
| 912 |
ids.push(id); produced++;
|
| 913 |
ui.out.textContent = Tokenizer.decode(tok, ids);
|
| 914 |
ui.tokcount.textContent = String(produced);
|
|
|
|
| 935 |
await sendTo(plan[1].id, packAct(seq, produced, plan[1].index, x));
|
| 936 |
hops += plan.length;
|
| 937 |
const key = `${seq}:${produced}`;
|
| 938 |
+
scheduleQueued();
|
| 939 |
const ok = await waitFor(() => actIn.has(key) || peerDropped, 60000);
|
| 940 |
if (peerDropped) throw new Error("peer dropped");
|
| 941 |
if (!ok) throw new Error("stalled");
|
| 942 |
x = actIn.get(key); actIn.delete(key);
|
| 943 |
}
|
| 944 |
const rows = Infer.draftRows(T, frame.shown, draft.length);
|
| 945 |
+
const logitRows = await Infer.logitsAt(stageObj, x, rows);
|
| 946 |
+
const picked = logitRows.map((l, i) => Infer.sampleAt(
|
| 947 |
+
l, opts, ids.length + i, opts.decodeSeq, ids.slice(promptLen).concat(draft.slice(0, i))));
|
| 948 |
const got = Shard.acceptDraft(picked, draft);
|
| 949 |
if (draft.length) log(`draft span ${draft.length}, accepted ${Math.max(0, got.length - 1)} before the bonus token. One lap.`);
|
| 950 |
for (const id of got) {
|
|
|
|
| 969 |
const all = { id: myId, index: 0, lo: 0, hi: spec.layers, head: true };
|
| 970 |
const solo = await loadMyStage({ repo: repoInfo.repo, revision: repoInfo.revision, spec, ctx: T }, all);
|
| 971 |
const promptIds = Tokenizer.encode(tok, ui.prompt.value || "The");
|
| 972 |
+
const verifyOpts = {
|
| 973 |
+
temperature: temp, topK: 40, seqSeed: +ui.seed.value | 0, schema,
|
| 974 |
+
decodeSeq: (arr) => Tokenizer.decode(tok, arr),
|
| 975 |
+
promptLen: promptIds.length ? promptIds.length : 1, maxDraft: 4,
|
| 976 |
+
};
|
| 977 |
const ref = useChunks
|
| 978 |
+
? await Infer.generateChunked([solo], promptIds.length ? promptIds : [0], nTok, 16, verifyOpts)
|
| 979 |
+
: await Infer.generateLookahead([solo], promptIds.length ? promptIds : [0], nTok, T, verifyOpts);
|
|
|
|
|
|
|
| 980 |
const same = ref.length === ids.length && ref.every((v, i) => v === ids[i]);
|
| 981 |
log(same ? "VERIFIED: the distributed run and the single-device run produced identical token ids."
|
| 982 |
: `MISMATCH: the ring and this device disagree. Distributed "${Tokenizer.decode(tok, ids).slice(0, 60)}" ` +
|
public/arch.js
CHANGED
|
@@ -63,7 +63,16 @@
|
|
| 63 |
// GPT-2's Conv1D stores (in, out) — already the k x n our GEMM wants.
|
| 64 |
// torch.nn.Linear stores (out, in) and must be transposed at load.
|
| 65 |
weightLayout: family === "gpt2" ? "in_out" : "out_in",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
if (spec.gated && !cfg.intermediate_size)
|
| 68 |
throw new Error("a gated MLP needs intermediate_size in config.json");
|
| 69 |
if (spec.kvHeads && spec.heads % spec.kvHeads !== 0)
|
|
|
|
| 63 |
// GPT-2's Conv1D stores (in, out) — already the k x n our GEMM wants.
|
| 64 |
// torch.nn.Linear stores (out, in) and must be transposed at load.
|
| 65 |
weightLayout: family === "gpt2" ? "in_out" : "out_in",
|
| 66 |
+
// Sliding window is mask-only. 0 means full causal attention.
|
| 67 |
+
// Layers with index < swaFrom stay full even when a window is set
|
| 68 |
+
// (Qwen2's max_window_layers). Compact KV truncation is not done here.
|
| 69 |
+
slidingWindow: 0,
|
| 70 |
+
swaFrom: 0,
|
| 71 |
};
|
| 72 |
+
if (family === "llama" && cfg.use_sliding_window && +cfg.sliding_window > 0) {
|
| 73 |
+
spec.slidingWindow = cfg.sliding_window | 0;
|
| 74 |
+
spec.swaFrom = cfg.max_window_layers != null ? cfg.max_window_layers | 0 : 0;
|
| 75 |
+
}
|
| 76 |
if (spec.gated && !cfg.intermediate_size)
|
| 77 |
throw new Error("a gated MLP needs intermediate_size in config.json");
|
| 78 |
if (spec.kvHeads && spec.heads % spec.kvHeads !== 0)
|
public/index.html
CHANGED
|
@@ -177,6 +177,12 @@
|
|
| 177 |
<p class="note" style="margin:.3rem 0 .6rem">0 = greedy, which is deterministic and therefore checkable. Above 0, sampling follows the seed so runs stay reproducible.</p>
|
| 178 |
<label class="slbl">Seed</label>
|
| 179 |
<input type="number" id="seed" value="1234" style="width:140px">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
<div class="row" id="verifyRow" style="display:none;margin-top:10px">
|
| 181 |
<label class="note" style="display:flex;gap:8px;align-items:center;cursor:pointer">
|
| 182 |
<input type="checkbox" id="verify"> Verify afterwards against a single-device run
|
|
|
|
| 177 |
<p class="note" style="margin:.3rem 0 .6rem">0 = greedy, which is deterministic and therefore checkable. Above 0, sampling follows the seed so runs stay reproducible.</p>
|
| 178 |
<label class="slbl">Seed</label>
|
| 179 |
<input type="number" id="seed" value="1234" style="width:140px">
|
| 180 |
+
<label class="slbl">Output</label>
|
| 181 |
+
<select id="schema" style="width:140px">
|
| 182 |
+
<option value="">free</option>
|
| 183 |
+
<option value="json">JSON</option>
|
| 184 |
+
</select>
|
| 185 |
+
<p class="note" style="margin:.3rem 0 .6rem">JSON constrains the completion only. The mask checks the top 40 tokens. The hidden state stays the same. Above temperature 0, the seed is tied to the token position.</p>
|
| 186 |
<div class="row" id="verifyRow" style="display:none;margin-top:10px">
|
| 187 |
<label class="note" style="display:flex;gap:8px;align-items:center;cursor:pointer">
|
| 188 |
<input type="checkbox" id="verify"> Verify afterwards against a single-device run
|
public/infer.js
CHANGED
|
@@ -180,13 +180,17 @@
|
|
| 180 |
const khT = TC.transpose(kh, N, hd); // hd x N
|
| 181 |
const scores = await V.vgemmBlock(qh, khT, { m: T, k: hd, n: N, batch: 1 }, ctx.L, ctx.bgemm, ctx.audit);
|
| 182 |
const a = new Float32Array(T * N); // causal softmax
|
|
|
|
| 183 |
for (let i = 0; i < T; i++) {
|
| 184 |
const see = n0 + i; // this query's own position in the cache
|
|
|
|
|
|
|
|
|
|
| 185 |
let mx = -Infinity;
|
| 186 |
-
for (let j =
|
| 187 |
let z = 0;
|
| 188 |
-
for (let j =
|
| 189 |
-
for (let j =
|
| 190 |
}
|
| 191 |
const oh = await V.vgemmBlock(a, vh, { m: T, k: N, n: hd, batch: 1 }, ctx.L, ctx.bgemm, ctx.audit);
|
| 192 |
for (let t = 0; t < T; t++) ctxOut.set(oh.subarray(t * hd, t * hd + hd), t * qDim + h * hd);
|
|
@@ -217,8 +221,13 @@
|
|
| 217 |
}
|
| 218 |
|
| 219 |
// ---- stage ------------------------------------------------------------------
|
| 220 |
-
function makeStage(spec, st, w, ctx, T) {
|
| 221 |
const S = { spec, st, w, ctx, T: T || Math.min(spec.maxPos, 128) };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
if (spec.rope) S.rope = ropeTables(spec.headDim, spec.ropeTheta, S.T + 1);
|
| 223 |
return S;
|
| 224 |
}
|
|
@@ -236,12 +245,39 @@
|
|
| 236 |
return x;
|
| 237 |
}
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
async function runLayers(S, x) {
|
| 240 |
// A non-finite activation used to quantize to a silent zero and keep
|
| 241 |
// travelling. Train refuses it (NonFiniteError). A hop must too: the
|
| 242 |
// next stage would otherwise agree on a wrong sum.
|
| 243 |
V.assertFinite(x, "activation");
|
| 244 |
-
|
| 245 |
V.assertFinite(x, "activation");
|
| 246 |
return x;
|
| 247 |
}
|
|
@@ -254,10 +290,7 @@
|
|
| 254 |
const C = S.spec.hidden;
|
| 255 |
const T = (x.length / C) | 0;
|
| 256 |
if (T < 1 || T * C !== x.length) throw new Error("chunk length is not a whole number of rows");
|
| 257 |
-
|
| 258 |
-
span.li = li;
|
| 259 |
-
x = await layerForward(S, x, S.w.layers[li], span);
|
| 260 |
-
}
|
| 261 |
V.assertFinite(x, "activation");
|
| 262 |
return x;
|
| 263 |
}
|
|
@@ -319,7 +352,8 @@
|
|
| 319 |
}
|
| 320 |
for (let n = 0; n < nNew; n++) {
|
| 321 |
const row = (last.length / stages[0].spec.hidden) - 1;
|
| 322 |
-
const id =
|
|
|
|
| 323 |
ids.push(id);
|
| 324 |
const pos = ids.length - 1;
|
| 325 |
let x = embedRows(stages[0], [id], pos);
|
|
@@ -347,6 +381,12 @@
|
|
| 347 |
|
| 348 |
// ---- sampling ---------------------------------------------------------------
|
| 349 |
function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
function pickToken(logits, opts) {
|
| 351 |
const o = opts || {}, temp = o.temperature ?? 0, vocab = logits.length;
|
| 352 |
if (!temp) {
|
|
@@ -355,16 +395,55 @@
|
|
| 355 |
return best;
|
| 356 |
}
|
| 357 |
const k = Math.min(o.topK || 40, vocab);
|
| 358 |
-
const idx = Array.from({ length: vocab }, (_, i) => i).sort((a, b) => logits[b] - logits[a]).slice(0, k);
|
| 359 |
let mx = -Infinity;
|
| 360 |
for (const i of idx) mx = Math.max(mx, logits[i] / temp);
|
| 361 |
let z = 0;
|
| 362 |
const p = idx.map(i => { const e = Math.exp(logits[i] / temp - mx); z += e; return e; });
|
| 363 |
-
|
|
|
|
| 364 |
for (let i = 0; i < idx.length; i++) { r -= p[i]; if (r <= 0) return idx[i]; }
|
| 365 |
return idx[idx.length - 1];
|
| 366 |
}
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
// ---- single-device reference ------------------------------------------------
|
| 369 |
async function generateLocal(stages, ids, nTokens, opts) {
|
| 370 |
const head = stages[0], T = head.T;
|
|
@@ -386,6 +465,8 @@
|
|
| 386 |
const ids = [...ids0];
|
| 387 |
const head = stages[0];
|
| 388 |
const maxDraft = opts && opts.maxDraft != null ? opts.maxDraft : 4;
|
|
|
|
|
|
|
| 389 |
let produced = 0;
|
| 390 |
while (produced < nNew) {
|
| 391 |
const frame = SH.lookaheadFrame(ids, T, maxDraft);
|
|
@@ -396,7 +477,11 @@
|
|
| 396 |
for (const S of stages) x = await runLayers(S, x);
|
| 397 |
const rows = draftRows(T, frame.shown, draft.length);
|
| 398 |
const picked = [];
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
const got = SH.acceptDraft(picked, draft);
|
| 401 |
if (!got.length) break;
|
| 402 |
for (const id of got) {
|
|
@@ -410,7 +495,7 @@
|
|
| 410 |
|
| 411 |
const api = { makeStage, embed, embedRows, runLayers, runLayersChunk, readout, logitsAt, draftRows,
|
| 412 |
generateLookahead,
|
| 413 |
-
pickToken, generateLocal, generateChunked,
|
| 414 |
rmsNorm, layerNorm, norm, ropeTables, applyRope, silu, gelu, mulberry32 };
|
| 415 |
if (typeof module !== "undefined" && module.exports) {
|
| 416 |
TC = require("./traincore.js"); V = require("./verified_core.js"); SH = require("./shard.js");
|
|
|
|
| 180 |
const khT = TC.transpose(kh, N, hd); // hd x N
|
| 181 |
const scores = await V.vgemmBlock(qh, khT, { m: T, k: hd, n: N, batch: 1 }, ctx.L, ctx.bgemm, ctx.audit);
|
| 182 |
const a = new Float32Array(T * N); // causal softmax
|
| 183 |
+
const swa = ly.swa | 0;
|
| 184 |
for (let i = 0; i < T; i++) {
|
| 185 |
const see = n0 + i; // this query's own position in the cache
|
| 186 |
+
// Mask mode only. Columns outside the window stay in the GEMM, so
|
| 187 |
+
// per-column int8 scales match a full causal matrix. Softmax drops them.
|
| 188 |
+
const oldest = swa > 0 ? Math.max(0, see - (swa - 1)) : 0;
|
| 189 |
let mx = -Infinity;
|
| 190 |
+
for (let j = oldest; j <= see; j++) mx = Math.max(mx, scores[i * N + j] * scale);
|
| 191 |
let z = 0;
|
| 192 |
+
for (let j = oldest; j <= see; j++) { const e = Math.exp(scores[i * N + j] * scale - mx); a[i * N + j] = e; z += e; }
|
| 193 |
+
for (let j = oldest; j <= see; j++) a[i * N + j] /= z;
|
| 194 |
}
|
| 195 |
const oh = await V.vgemmBlock(a, vh, { m: T, k: N, n: hd, batch: 1 }, ctx.L, ctx.bgemm, ctx.audit);
|
| 196 |
for (let t = 0; t < T; t++) ctxOut.set(oh.subarray(t * hd, t * hd + hd), t * qDim + h * hd);
|
|
|
|
| 221 |
}
|
| 222 |
|
| 223 |
// ---- stage ------------------------------------------------------------------
|
| 224 |
+
function makeStage(spec, st, w, ctx, T, opts) {
|
| 225 |
const S = { spec, st, w, ctx, T: T || Math.min(spec.maxPos, 128) };
|
| 226 |
+
// Two or more layers: one flat parameter shard, gathered per layer.
|
| 227 |
+
// opts.resident keeps the old layout for the bit-identity check.
|
| 228 |
+
const resident = opts && opts.resident;
|
| 229 |
+
S.paramShard = (!resident && w.layers && w.layers.length >= 2)
|
| 230 |
+
? SH.makeParamShard(w.layers) : null;
|
| 231 |
if (spec.rope) S.rope = ropeTables(spec.headDim, spec.ropeTheta, S.T + 1);
|
| 232 |
return S;
|
| 233 |
}
|
|
|
|
| 245 |
return x;
|
| 246 |
}
|
| 247 |
|
| 248 |
+
// Walk this stage's layers. With a parameter shard, gather layer i, prefetch
|
| 249 |
+
// i+1, run the same layerForward, then release i. At most two layers hold
|
| 250 |
+
// views. Without a shard the arrays stay on the layer objects.
|
| 251 |
+
async function forwardLayers(S, x, span) {
|
| 252 |
+
const layers = S.w.layers;
|
| 253 |
+
const shard = S.paramShard;
|
| 254 |
+
if (!shard) {
|
| 255 |
+
if (span) {
|
| 256 |
+
for (let li = 0; li < layers.length; li++) {
|
| 257 |
+
span.li = li;
|
| 258 |
+
x = await layerForward(S, x, layers[li], span);
|
| 259 |
+
}
|
| 260 |
+
} else {
|
| 261 |
+
for (const ly of layers) x = await layerForward(S, x, ly);
|
| 262 |
+
}
|
| 263 |
+
return x;
|
| 264 |
+
}
|
| 265 |
+
for (let li = 0; li < layers.length; li++) {
|
| 266 |
+
SH.gatherLayer(shard, layers, li);
|
| 267 |
+
if (li + 1 < layers.length) SH.gatherLayer(shard, layers, li + 1);
|
| 268 |
+
if (span) span.li = li;
|
| 269 |
+
x = await layerForward(S, x, layers[li], span || undefined);
|
| 270 |
+
SH.releaseLayer(shard, layers, li);
|
| 271 |
+
}
|
| 272 |
+
return x;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
async function runLayers(S, x) {
|
| 276 |
// A non-finite activation used to quantize to a silent zero and keep
|
| 277 |
// travelling. Train refuses it (NonFiniteError). A hop must too: the
|
| 278 |
// next stage would otherwise agree on a wrong sum.
|
| 279 |
V.assertFinite(x, "activation");
|
| 280 |
+
x = await forwardLayers(S, x, null);
|
| 281 |
V.assertFinite(x, "activation");
|
| 282 |
return x;
|
| 283 |
}
|
|
|
|
| 290 |
const C = S.spec.hidden;
|
| 291 |
const T = (x.length / C) | 0;
|
| 292 |
if (T < 1 || T * C !== x.length) throw new Error("chunk length is not a whole number of rows");
|
| 293 |
+
x = await forwardLayers(S, x, span);
|
|
|
|
|
|
|
|
|
|
| 294 |
V.assertFinite(x, "activation");
|
| 295 |
return x;
|
| 296 |
}
|
|
|
|
| 352 |
}
|
| 353 |
for (let n = 0; n < nNew; n++) {
|
| 354 |
const row = (last.length / stages[0].spec.hidden) - 1;
|
| 355 |
+
const id = sampleAt((await logitsAt(stages[0], last, [row]))[0], opts, ids.length,
|
| 356 |
+
opts && opts.decodeSeq, ids.slice(ids0.length));
|
| 357 |
ids.push(id);
|
| 358 |
const pos = ids.length - 1;
|
| 359 |
let x = embedRows(stages[0], [id], pos);
|
|
|
|
| 381 |
|
| 382 |
// ---- sampling ---------------------------------------------------------------
|
| 383 |
function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
|
| 384 |
+
// One draw for absolute position `pos`. A longer or shorter draft does not
|
| 385 |
+
// move the draw for a later position.
|
| 386 |
+
function seqDraw(seed, pos) {
|
| 387 |
+
let a = (seed | 0) ^ Math.imul((pos | 0) + 1, 0x9E3779B1);
|
| 388 |
+
return mulberry32(a)();
|
| 389 |
+
}
|
| 390 |
function pickToken(logits, opts) {
|
| 391 |
const o = opts || {}, temp = o.temperature ?? 0, vocab = logits.length;
|
| 392 |
if (!temp) {
|
|
|
|
| 395 |
return best;
|
| 396 |
}
|
| 397 |
const k = Math.min(o.topK || 40, vocab);
|
| 398 |
+
const idx = Array.from({ length: vocab }, (_, i) => i).sort((a, b) => logits[b] - logits[a] || a - b).slice(0, k);
|
| 399 |
let mx = -Infinity;
|
| 400 |
for (const i of idx) mx = Math.max(mx, logits[i] / temp);
|
| 401 |
let z = 0;
|
| 402 |
const p = idx.map(i => { const e = Math.exp(logits[i] / temp - mx); z += e; return e; });
|
| 403 |
+
const draw = (o.seqSeed != null && o.pos != null) ? () => seqDraw(o.seqSeed, o.pos) : (o.rng || Math.random);
|
| 404 |
+
let r = draw() * z;
|
| 405 |
for (let i = 0; i < idx.length; i++) { r -= p[i]; if (r <= 0) return idx[i]; }
|
| 406 |
return idx[idx.length - 1];
|
| 407 |
}
|
| 408 |
|
| 409 |
+
// Head-side JSON mask. The hidden state is already computed. Only the pick
|
| 410 |
+
// changes, and only among the top-K logits. No legal candidate falls back
|
| 411 |
+
// to the unconstrained pick.
|
| 412 |
+
function pickStructured(logits, opts) {
|
| 413 |
+
const o = opts || {};
|
| 414 |
+
if (o.schema !== "json" || typeof o.decodePiece !== "function") return pickToken(logits, o);
|
| 415 |
+
const produced = o.produced || "";
|
| 416 |
+
const vocab = logits.length;
|
| 417 |
+
const k = Math.min(o.topK || 40, vocab);
|
| 418 |
+
const idx = Array.from({ length: vocab }, (_, i) => i).sort((a, b) => logits[b] - logits[a] || a - b).slice(0, k);
|
| 419 |
+
const legal = [];
|
| 420 |
+
for (const id of idx) {
|
| 421 |
+
let piece;
|
| 422 |
+
try { piece = o.decodePiece(id); } catch (e) { continue; }
|
| 423 |
+
if (piece == null) continue;
|
| 424 |
+
if (SH.jsonAllows(produced, piece)) legal.push(id);
|
| 425 |
+
}
|
| 426 |
+
if (!legal.length) return pickToken(logits, o);
|
| 427 |
+
if (!(o.temperature ?? 0)) return legal[0];
|
| 428 |
+
const fake = new Float32Array(vocab);
|
| 429 |
+
fake.fill(-Infinity);
|
| 430 |
+
for (const id of legal) fake[id] = logits[id];
|
| 431 |
+
return pickToken(fake, o);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
function sampleAt(logits, opts, pos, decodeSeq, baseIds) {
|
| 435 |
+
const local = Object.assign({}, opts || {}, { pos: pos | 0 });
|
| 436 |
+
if (local.schema === "json" && typeof decodeSeq === "function" && baseIds) {
|
| 437 |
+
local.produced = decodeSeq(baseIds);
|
| 438 |
+
local.decodePiece = (id) => {
|
| 439 |
+
const prev = local.produced;
|
| 440 |
+
const next = decodeSeq(baseIds.concat([id]));
|
| 441 |
+
return next.startsWith(prev) ? next.slice(prev.length) : next;
|
| 442 |
+
};
|
| 443 |
+
}
|
| 444 |
+
return pickStructured(logits, local);
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
// ---- single-device reference ------------------------------------------------
|
| 448 |
async function generateLocal(stages, ids, nTokens, opts) {
|
| 449 |
const head = stages[0], T = head.T;
|
|
|
|
| 465 |
const ids = [...ids0];
|
| 466 |
const head = stages[0];
|
| 467 |
const maxDraft = opts && opts.maxDraft != null ? opts.maxDraft : 4;
|
| 468 |
+
const promptLen = opts && opts.promptLen != null ? opts.promptLen | 0 : ids0.length;
|
| 469 |
+
const decodeSeq = opts && opts.decodeSeq;
|
| 470 |
let produced = 0;
|
| 471 |
while (produced < nNew) {
|
| 472 |
const frame = SH.lookaheadFrame(ids, T, maxDraft);
|
|
|
|
| 477 |
for (const S of stages) x = await runLayers(S, x);
|
| 478 |
const rows = draftRows(T, frame.shown, draft.length);
|
| 479 |
const picked = [];
|
| 480 |
+
const logitRows = await logitsAt(head, x, rows);
|
| 481 |
+
for (let i = 0; i < logitRows.length; i++) {
|
| 482 |
+
const base = ids.slice(promptLen).concat(draft.slice(0, i));
|
| 483 |
+
picked.push(sampleAt(logitRows[i], opts, ids.length + i, decodeSeq, base));
|
| 484 |
+
}
|
| 485 |
const got = SH.acceptDraft(picked, draft);
|
| 486 |
if (!got.length) break;
|
| 487 |
for (const id of got) {
|
|
|
|
| 495 |
|
| 496 |
const api = { makeStage, embed, embedRows, runLayers, runLayersChunk, readout, logitsAt, draftRows,
|
| 497 |
generateLookahead,
|
| 498 |
+
pickToken, pickStructured, sampleAt, seqDraw, generateLocal, generateChunked,
|
| 499 |
rmsNorm, layerNorm, norm, ropeTables, applyRope, silu, gelu, mulberry32 };
|
| 500 |
if (typeof module !== "undefined" && module.exports) {
|
| 501 |
TC = require("./traincore.js"); V = require("./verified_core.js"); SH = require("./shard.js");
|
public/shard.js
CHANGED
|
@@ -114,6 +114,8 @@
|
|
| 114 |
ly.Wfc = toKN(pick(t.Wfc), spec.inter, C, L); ly.bfc = pick(t.bfc, true);
|
| 115 |
}
|
| 116 |
ly.Wdown = toKN(pick(t.Wdown), C, spec.inter, L); ly.bdown = pick(t.bdown, true);
|
|
|
|
|
|
|
| 117 |
w.layers.push(ly);
|
| 118 |
}
|
| 119 |
return w;
|
|
@@ -125,6 +127,68 @@
|
|
| 125 |
return out;
|
| 126 |
}
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
// ---- hashes -----------------------------------------------------------------
|
| 129 |
// FNV-1a over raw bytes, identical to the function DaisyChain-Web hashes
|
| 130 |
// replicas with, so hashes stay comparable across the projects.
|
|
@@ -333,6 +397,136 @@
|
|
| 333 |
return { draft, shown: shown.length, win: shown.slice() };
|
| 334 |
}
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
function acceptDraft(picked, draft) {
|
| 337 |
const out = [];
|
| 338 |
let i = 0;
|
|
@@ -345,7 +539,8 @@
|
|
| 345 |
fnv1a, hashF32, modelFingerprint, ringMembers, estimateLapMs,
|
| 346 |
headCanHold, preferLocal, makePrefixIndex, makeKvSpill,
|
| 347 |
prefillChunks, promptLookupDraft, acceptDraft, lookaheadFrame,
|
| 348 |
-
estimatePlanMs
|
|
|
|
| 349 |
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
| 350 |
else root.Shard = api;
|
| 351 |
})(typeof self !== "undefined" ? self : this);
|
|
|
|
| 114 |
ly.Wfc = toKN(pick(t.Wfc), spec.inter, C, L); ly.bfc = pick(t.bfc, true);
|
| 115 |
}
|
| 116 |
ly.Wdown = toKN(pick(t.Wdown), C, spec.inter, L); ly.bdown = pick(t.bdown, true);
|
| 117 |
+
const sw = spec.slidingWindow | 0;
|
| 118 |
+
ly.swa = sw > 0 && l >= (spec.swaFrom | 0) ? sw : 0;
|
| 119 |
w.layers.push(ly);
|
| 120 |
}
|
| 121 |
return w;
|
|
|
|
| 127 |
return out;
|
| 128 |
}
|
| 129 |
|
| 130 |
+
// ---- parameter shard (ZeRO-3 / FSDP FULL_SHARD, forward only) --------------
|
| 131 |
+
// A stage that owns two or more layers keeps those weights in one flat
|
| 132 |
+
// Float32 buffer. A forward gathers the current layer as views into that
|
| 133 |
+
// buffer, prefetches the next, runs it, then drops the views. At most two
|
| 134 |
+
// layers are unpacked (FSDP limit_all_gathers + forward_prefetch +
|
| 135 |
+
// reshard-after-forward). The flat buffer is the CPU-resident copy.
|
| 136 |
+
// Embeddings, the final norm, and lm_head are not in this shard.
|
| 137 |
+
// The views alias the buffer. The GEMM only reads them, so the bytes the
|
| 138 |
+
// kernel sees are the bytes that were resident. No optimizer state, no
|
| 139 |
+
// gradient, and no copy of a parameter is sent to a peer.
|
| 140 |
+
const PARAM_KEYS = ["nrm1", "nrm1b", "nrm2", "nrm2b",
|
| 141 |
+
"Wqkv", "bqkv", "Wq", "bq", "Wk", "bk", "Wv", "bv",
|
| 142 |
+
"Wo", "bo", "Wgate", "Wup", "Wfc", "bfc", "Wdown", "bdown"];
|
| 143 |
+
const GATHER_LIMIT = 2;
|
| 144 |
+
|
| 145 |
+
function makeParamShard(layers) {
|
| 146 |
+
if (!layers || layers.length < 2) return null;
|
| 147 |
+
const slices = [];
|
| 148 |
+
let n = 0;
|
| 149 |
+
for (let i = 0; i < layers.length; i++) {
|
| 150 |
+
const parts = [];
|
| 151 |
+
const ly = layers[i];
|
| 152 |
+
for (const k of PARAM_KEYS) {
|
| 153 |
+
const a = ly[k];
|
| 154 |
+
if (a && a.length) {
|
| 155 |
+
parts.push({ k, off: n, len: a.length });
|
| 156 |
+
n += a.length;
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
slices.push(parts);
|
| 160 |
+
}
|
| 161 |
+
const flat = new Float32Array(n);
|
| 162 |
+
for (let i = 0; i < layers.length; i++)
|
| 163 |
+
for (const p of slices[i]) flat.set(layers[i][p.k], p.off);
|
| 164 |
+
for (let i = 0; i < layers.length; i++) {
|
| 165 |
+
for (const p of slices[i]) layers[i][p.k] = null;
|
| 166 |
+
layers[i]._gathered = false;
|
| 167 |
+
}
|
| 168 |
+
return { flat, slices, live: 0, maxLive: 0, limit: GATHER_LIMIT };
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function gatherLayer(shard, layers, i) {
|
| 172 |
+
const ly = layers[i];
|
| 173 |
+
if (ly._gathered) return ly;
|
| 174 |
+
if (shard.live >= shard.limit)
|
| 175 |
+
throw new Error("param gather would exceed " + shard.limit + " live layers");
|
| 176 |
+
for (const p of shard.slices[i])
|
| 177 |
+
ly[p.k] = shard.flat.subarray(p.off, p.off + p.len);
|
| 178 |
+
ly._gathered = true;
|
| 179 |
+
shard.live++;
|
| 180 |
+
if (shard.live > shard.maxLive) shard.maxLive = shard.live;
|
| 181 |
+
return ly;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function releaseLayer(shard, layers, i) {
|
| 185 |
+
const ly = layers[i];
|
| 186 |
+
if (!ly || !ly._gathered) return;
|
| 187 |
+
for (const p of shard.slices[i]) ly[p.k] = null;
|
| 188 |
+
ly._gathered = false;
|
| 189 |
+
shard.live--;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
// ---- hashes -----------------------------------------------------------------
|
| 193 |
// FNV-1a over raw bytes, identical to the function DaisyChain-Web hashes
|
| 194 |
// replicas with, so hashes stay comparable across the projects.
|
|
|
|
| 397 |
return { draft, shown: shown.length, win: shown.slice() };
|
| 398 |
}
|
| 399 |
|
| 400 |
+
// The text of a prompt that was tokenized while an earlier turn still held
|
| 401 |
+
// the ring. Used only when the text still matches. Returns a copy.
|
| 402 |
+
function preparedIds(text, prepared) {
|
| 403 |
+
if (!prepared || prepared.text !== text || !prepared.ids || !prepared.ids.length) return null;
|
| 404 |
+
return prepared.ids.slice();
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
// JSON value scanner for a completion constraint. "ok" is a finished value,
|
| 408 |
+
// "need" is a legal prefix, "bad" is a broken one. Incomplete is allowed.
|
| 409 |
+
function jsonStatus(text) {
|
| 410 |
+
const s = String(text || "");
|
| 411 |
+
let i = 0;
|
| 412 |
+
const n = s.length;
|
| 413 |
+
function ws() {
|
| 414 |
+
while (i < n && (s[i] === " " || s[i] === "\n" || s[i] === "\r" || s[i] === "\t")) i++;
|
| 415 |
+
}
|
| 416 |
+
function string() {
|
| 417 |
+
i++;
|
| 418 |
+
while (i < n) {
|
| 419 |
+
const c = s[i++];
|
| 420 |
+
if (c === "\"") return "ok";
|
| 421 |
+
if (c === "\\") {
|
| 422 |
+
if (i >= n) return "need";
|
| 423 |
+
const e = s[i++];
|
| 424 |
+
if ("\"\\/bfnrt".indexOf(e) >= 0) continue;
|
| 425 |
+
if (e === "u") {
|
| 426 |
+
if (i + 4 > n) return "need";
|
| 427 |
+
const hex = s.slice(i, i + 4);
|
| 428 |
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return "bad";
|
| 429 |
+
i += 4;
|
| 430 |
+
continue;
|
| 431 |
+
}
|
| 432 |
+
return "bad";
|
| 433 |
+
}
|
| 434 |
+
if (c < " ") return "bad";
|
| 435 |
+
}
|
| 436 |
+
return "need";
|
| 437 |
+
}
|
| 438 |
+
function number() {
|
| 439 |
+
if (s[i] === "-") i++;
|
| 440 |
+
if (i >= n) return "need";
|
| 441 |
+
if (s[i] === "0") i++;
|
| 442 |
+
else if (s[i] >= "1" && s[i] <= "9") { while (i < n && s[i] >= "0" && s[i] <= "9") i++; }
|
| 443 |
+
else return "bad";
|
| 444 |
+
if (i >= n) return "need";
|
| 445 |
+
if (s[i] === ".") {
|
| 446 |
+
i++;
|
| 447 |
+
if (i >= n) return "need";
|
| 448 |
+
if (s[i] < "0" || s[i] > "9") return "bad";
|
| 449 |
+
while (i < n && s[i] >= "0" && s[i] <= "9") i++;
|
| 450 |
+
}
|
| 451 |
+
if (i < n && (s[i] === "e" || s[i] === "E")) {
|
| 452 |
+
i++;
|
| 453 |
+
if (i >= n) return "need";
|
| 454 |
+
if (s[i] === "+" || s[i] === "-") i++;
|
| 455 |
+
if (i >= n) return "need";
|
| 456 |
+
if (s[i] < "0" || s[i] > "9") return "bad";
|
| 457 |
+
while (i < n && s[i] >= "0" && s[i] <= "9") i++;
|
| 458 |
+
}
|
| 459 |
+
return "ok";
|
| 460 |
+
}
|
| 461 |
+
function literal() {
|
| 462 |
+
const rest = s.slice(i);
|
| 463 |
+
const words = ["true", "false", "null"];
|
| 464 |
+
for (let w = 0; w < words.length; w++) {
|
| 465 |
+
const word = words[w];
|
| 466 |
+
if (rest.indexOf(word) === 0) { i += word.length; return "ok"; }
|
| 467 |
+
if (word.indexOf(rest) === 0) return "need";
|
| 468 |
+
}
|
| 469 |
+
return "bad";
|
| 470 |
+
}
|
| 471 |
+
function value() {
|
| 472 |
+
ws();
|
| 473 |
+
if (i >= n) return "need";
|
| 474 |
+
const c = s[i];
|
| 475 |
+
if (c === "{") return object();
|
| 476 |
+
if (c === "[") return array();
|
| 477 |
+
if (c === "\"") return string();
|
| 478 |
+
if (c === "-" || (c >= "0" && c <= "9")) return number();
|
| 479 |
+
return literal();
|
| 480 |
+
}
|
| 481 |
+
function object() {
|
| 482 |
+
i++;
|
| 483 |
+
ws();
|
| 484 |
+
if (i >= n) return "need";
|
| 485 |
+
if (s[i] === "}") { i++; return "ok"; }
|
| 486 |
+
for (;;) {
|
| 487 |
+
ws();
|
| 488 |
+
if (i >= n) return "need";
|
| 489 |
+
if (s[i] !== "\"") return "bad";
|
| 490 |
+
const key = string();
|
| 491 |
+
if (key !== "ok") return key;
|
| 492 |
+
ws();
|
| 493 |
+
if (i >= n) return "need";
|
| 494 |
+
if (s[i] !== ":") return "bad";
|
| 495 |
+
i++;
|
| 496 |
+
const v = value();
|
| 497 |
+
if (v !== "ok") return v;
|
| 498 |
+
ws();
|
| 499 |
+
if (i >= n) return "need";
|
| 500 |
+
if (s[i] === "}") { i++; return "ok"; }
|
| 501 |
+
if (s[i] !== ",") return "bad";
|
| 502 |
+
i++;
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
function array() {
|
| 506 |
+
i++;
|
| 507 |
+
ws();
|
| 508 |
+
if (i >= n) return "need";
|
| 509 |
+
if (s[i] === "]") { i++; return "ok"; }
|
| 510 |
+
for (;;) {
|
| 511 |
+
const v = value();
|
| 512 |
+
if (v !== "ok") return v;
|
| 513 |
+
ws();
|
| 514 |
+
if (i >= n) return "need";
|
| 515 |
+
if (s[i] === "]") { i++; return "ok"; }
|
| 516 |
+
if (s[i] !== ",") return "bad";
|
| 517 |
+
i++;
|
| 518 |
+
}
|
| 519 |
+
}
|
| 520 |
+
const st = value();
|
| 521 |
+
if (st !== "ok") return st;
|
| 522 |
+
ws();
|
| 523 |
+
if (i < n) return "bad";
|
| 524 |
+
return "ok";
|
| 525 |
+
}
|
| 526 |
+
function jsonAllows(soFar, more) {
|
| 527 |
+
return jsonStatus(String(soFar || "") + String(more == null ? "" : more)) !== "bad";
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
function acceptDraft(picked, draft) {
|
| 531 |
const out = [];
|
| 532 |
let i = 0;
|
|
|
|
| 539 |
fnv1a, hashF32, modelFingerprint, ringMembers, estimateLapMs,
|
| 540 |
headCanHold, preferLocal, makePrefixIndex, makeKvSpill,
|
| 541 |
prefillChunks, promptLookupDraft, acceptDraft, lookaheadFrame,
|
| 542 |
+
estimatePlanMs, makeParamShard, gatherLayer, releaseLayer,
|
| 543 |
+
PARAM_KEYS, GATHER_LIMIT, preparedIds, jsonStatus, jsonAllows };
|
| 544 |
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
| 545 |
else root.Shard = api;
|
| 546 |
})(typeof self !== "undefined" ? self : this);
|
test_pipeline.js
CHANGED
|
@@ -279,6 +279,155 @@ const GPT2 = Arch.fromConfig({
|
|
| 279 |
assert.strictEqual(whole.length, prompt.length + 4);
|
| 280 |
});
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
await t("a non-finite activation is refused before any layer runs", async () => {
|
| 283 |
const S = { w: { layers: [] }, spec: LLAMA, T: 2 };
|
| 284 |
const bad = new Float32Array([0, 1, NaN, 2]);
|
|
|
|
| 279 |
assert.strictEqual(whole.length, prompt.length + 4);
|
| 280 |
});
|
| 281 |
|
| 282 |
+
await t("a parameter shard matches a resident forward and keeps at most 2 layers live", async () => {
|
| 283 |
+
for (const spec of [LLAMA, GPT2]) {
|
| 284 |
+
const got = makeWeights(spec);
|
| 285 |
+
const st = { id: "p0", index: 0, lo: 0, hi: spec.layers, head: true };
|
| 286 |
+
const wRes = Shard.stageWeights(spec, st, got, Arch);
|
| 287 |
+
const wSh = Shard.stageWeights(spec, st, got, Arch);
|
| 288 |
+
const resident = Infer.makeStage(spec, st, wRes, ctx(), T, { resident: true });
|
| 289 |
+
const sharded = Infer.makeStage(spec, st, wSh, ctx(), T);
|
| 290 |
+
assert.ok(sharded.paramShard, "a multi-layer stage should flat-shard");
|
| 291 |
+
assert.strictEqual(sharded.paramShard.limit, 2);
|
| 292 |
+
assert.strictEqual(resident.paramShard, null);
|
| 293 |
+
assert.ok(wSh.emb && wSh.emb.length, "embeddings stay outside the shard");
|
| 294 |
+
assert.ok(wSh.lmHead && wSh.lmHead.length, "lm_head stays resident");
|
| 295 |
+
const firstKey = sharded.paramShard.slices[0][0].k;
|
| 296 |
+
assert.strictEqual(wSh.layers[0][firstKey], null, "weights are packed, not left on the layer");
|
| 297 |
+
const win = new Int32Array(T);
|
| 298 |
+
for (let i = 0; i < PROMPT.length; i++) win[T - PROMPT.length + i] = PROMPT[i];
|
| 299 |
+
const a = await Infer.runLayers(resident, Infer.embed(resident, win));
|
| 300 |
+
const flatBefore = sharded.paramShard.flat.slice();
|
| 301 |
+
const b = await Infer.runLayers(sharded, Infer.embed(sharded, win));
|
| 302 |
+
assert.ok(bitsEqual(a, b), "sharded runLayers diverged from resident");
|
| 303 |
+
assert.strictEqual(sharded.paramShard.live, 0);
|
| 304 |
+
assert.strictEqual(sharded.paramShard.maxLive, 2);
|
| 305 |
+
assert.strictEqual(wSh.layers[0][firstKey], null);
|
| 306 |
+
assert.strictEqual(wSh.layers[spec.layers - 1][firstKey], null);
|
| 307 |
+
assert.ok(bitsEqual(flatBefore, sharded.paramShard.flat), "the forward wrote the flat shard");
|
| 308 |
+
for (let i = 0; i < wRes.layers.length; i++) {
|
| 309 |
+
for (const p of sharded.paramShard.slices[i]) {
|
| 310 |
+
const src = wRes.layers[i][p.k];
|
| 311 |
+
assert.strictEqual(src.length, p.len);
|
| 312 |
+
for (let j = 0; j < p.len; j++)
|
| 313 |
+
if (sharded.paramShard.flat[p.off + j] !== src[j])
|
| 314 |
+
throw new Error("flat shard byte mismatch at layer " + i);
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
const ids = [1, 2, 3, 4];
|
| 318 |
+
const spanR = { pos0: 0, cache: [] };
|
| 319 |
+
const spanS = { pos0: 0, cache: [] };
|
| 320 |
+
const cr = await Infer.runLayersChunk(resident, Infer.embedRows(resident, ids, 0), spanR);
|
| 321 |
+
const cs = await Infer.runLayersChunk(sharded, Infer.embedRows(sharded, ids, 0), spanS);
|
| 322 |
+
assert.ok(bitsEqual(cr, cs), "sharded chunk diverged from resident");
|
| 323 |
+
assert.ok(bitsEqual(spanR.cache[0].k, spanS.cache[0].k));
|
| 324 |
+
assert.ok(bitsEqual(spanR.cache[0].v, spanS.cache[0].v));
|
| 325 |
+
assert.strictEqual(sharded.paramShard.live, 0);
|
| 326 |
+
assert.ok(bitsEqual(flatBefore, sharded.paramShard.flat));
|
| 327 |
+
Shard.gatherLayer(sharded.paramShard, wSh.layers, 0);
|
| 328 |
+
Shard.gatherLayer(sharded.paramShard, wSh.layers, 1);
|
| 329 |
+
assert.throws(() => Shard.gatherLayer(sharded.paramShard, wSh.layers, 2), /2 live/);
|
| 330 |
+
Shard.releaseLayer(sharded.paramShard, wSh.layers, 0);
|
| 331 |
+
Shard.releaseLayer(sharded.paramShard, wSh.layers, 1);
|
| 332 |
+
assert.strictEqual(sharded.paramShard.live, 0);
|
| 333 |
+
}
|
| 334 |
+
const st1 = { id: "p0", index: 0, lo: 0, hi: 1, head: true };
|
| 335 |
+
const one = Infer.makeStage(LLAMA, st1, Shard.stageWeights(LLAMA, st1, makeWeights(LLAMA), Arch), ctx(), T);
|
| 336 |
+
assert.strictEqual(one.paramShard, null, "a single layer stays resident");
|
| 337 |
+
assert.ok(one.w.layers[0].Wq && one.w.layers[0].Wq.length);
|
| 338 |
+
assert.strictEqual(Shard.makeParamShard([one.w.layers[0]]), null);
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
await t("sliding-window mask matches across a split and stays full-width", async () => {
|
| 342 |
+
const got = makeWeights(LLAMA);
|
| 343 |
+
const win = new Int32Array(T);
|
| 344 |
+
for (let i = 0; i < PROMPT.length; i++) win[T - PROMPT.length + i] = PROMPT[i];
|
| 345 |
+
const run = async (spec, splits) => {
|
| 346 |
+
const stages = stagesFor(spec, got, splits, T);
|
| 347 |
+
let x = Infer.embed(stages[0], win);
|
| 348 |
+
for (const S of stages) x = await Infer.runLayers(S, x);
|
| 349 |
+
return x;
|
| 350 |
+
};
|
| 351 |
+
const plain = await run(LLAMA, [LLAMA.layers]);
|
| 352 |
+
const wide = Object.assign({}, LLAMA, { slidingWindow: 1000, swaFrom: 0 });
|
| 353 |
+
assert.ok(bitsEqual(plain, await run(wide, [LLAMA.layers])), "a window wider than the cache changed the GEMM");
|
| 354 |
+
const late = Object.assign({}, LLAMA, { slidingWindow: 2, swaFrom: LLAMA.layers });
|
| 355 |
+
assert.ok(bitsEqual(plain, await run(late, [LLAMA.layers])), "layers before swaFrom must stay full causal");
|
| 356 |
+
const sw = Object.assign({}, LLAMA, { slidingWindow: 2, swaFrom: 0 });
|
| 357 |
+
const masked = await run(sw, [sw.layers]);
|
| 358 |
+
assert.ok(!bitsEqual(plain, masked), "a window of 2 did not change attention");
|
| 359 |
+
const split = await run(sw, [2, 2, 2]);
|
| 360 |
+
assert.ok(bitsEqual(masked, split), "sliding-window split diverged");
|
| 361 |
+
const cfg = Arch.fromConfig({
|
| 362 |
+
model_type: "qwen2", hidden_size: 64, num_hidden_layers: 4, num_attention_heads: 4,
|
| 363 |
+
num_key_value_heads: 2, intermediate_size: 128, vocab_size: 32,
|
| 364 |
+
use_sliding_window: true, sliding_window: 8, max_window_layers: 2,
|
| 365 |
+
});
|
| 366 |
+
assert.strictEqual(cfg.slidingWindow, 8);
|
| 367 |
+
assert.strictEqual(cfg.swaFrom, 2);
|
| 368 |
+
const off = Arch.fromConfig({
|
| 369 |
+
model_type: "qwen2", hidden_size: 64, num_hidden_layers: 4, num_attention_heads: 4,
|
| 370 |
+
num_key_value_heads: 2, intermediate_size: 128, vocab_size: 32,
|
| 371 |
+
use_sliding_window: false, sliding_window: 8,
|
| 372 |
+
});
|
| 373 |
+
assert.strictEqual(off.slidingWindow, 0);
|
| 374 |
+
});
|
| 375 |
+
|
| 376 |
+
await t("JSON mask, position seed, and a prepared prompt", async () => {
|
| 377 |
+
assert.strictEqual(Shard.jsonStatus("{\"a\":1}"), "ok");
|
| 378 |
+
assert.strictEqual(Shard.jsonAllows("", "{"), true);
|
| 379 |
+
assert.strictEqual(Shard.jsonAllows("{", "a"), false);
|
| 380 |
+
assert.strictEqual(Shard.jsonAllows("{", "\"a\":"), true);
|
| 381 |
+
assert.strictEqual(Shard.jsonAllows("{\"a\":1}", "x"), false);
|
| 382 |
+
assert.strictEqual(Shard.jsonAllows("tru", "e"), true);
|
| 383 |
+
assert.strictEqual(Shard.jsonAllows("tru", "k"), false);
|
| 384 |
+
assert.strictEqual(Shard.jsonAllows("[1,", "2]"), true);
|
| 385 |
+
assert.strictEqual(Shard.jsonAllows("{\"a\":1,", "}"), false);
|
| 386 |
+
assert.strictEqual(Shard.jsonAllows("", " "), true);
|
| 387 |
+
const logits = Float32Array.from([5, 0.1, 1]);
|
| 388 |
+
const decodePiece = (id) => ["x", "{", "}"][id];
|
| 389 |
+
assert.strictEqual(Infer.pickStructured(logits, {
|
| 390 |
+
schema: "json", temperature: 0, topK: 3, produced: "", decodePiece,
|
| 391 |
+
}), 1);
|
| 392 |
+
assert.strictEqual(Infer.pickStructured(logits, { temperature: 0 }), 0);
|
| 393 |
+
const sample = Float32Array.from([0, 0.2, 0.5, 1.2, 0.1]);
|
| 394 |
+
const a = Infer.pickToken(sample, { temperature: 0.8, topK: 5, seqSeed: 99, pos: 4, rng: () => 0.1 });
|
| 395 |
+
const b = Infer.pickToken(sample, { temperature: 0.8, topK: 5, seqSeed: 99, pos: 4, rng: () => 0.9 });
|
| 396 |
+
assert.strictEqual(a, b, "a position seed must ignore the mutable rng");
|
| 397 |
+
assert.strictEqual(Infer.seqDraw(99, 4), Infer.seqDraw(99, 4));
|
| 398 |
+
assert.deepStrictEqual(Shard.preparedIds("hi", { text: "hi", ids: [1, 2] }), [1, 2]);
|
| 399 |
+
assert.strictEqual(Shard.preparedIds("no", { text: "hi", ids: [1] }), null);
|
| 400 |
+
const gotIds = Shard.preparedIds("hi", { text: "hi", ids: [1, 2] });
|
| 401 |
+
gotIds.push(9);
|
| 402 |
+
assert.deepStrictEqual(Shard.preparedIds("hi", { text: "hi", ids: [1, 2] }), [1, 2]);
|
| 403 |
+
});
|
| 404 |
+
|
| 405 |
+
await t("a 128-channel weight scale is not the verified column scale", async () => {
|
| 406 |
+
// DeepGEMM's FP8 GEMM scales every 128 channels of K (BLOCK_K == 128).
|
| 407 |
+
// vgemmBlock uses one scale for the whole column. A spike in a later
|
| 408 |
+
// block must not tighten the earlier block under our recipe, and it
|
| 409 |
+
// must widen the single column scale. The forward path does not call
|
| 410 |
+
// the block recipe.
|
| 411 |
+
const V = require("./public/verified_core.js");
|
| 412 |
+
const k = 256, n = 3, block = 128;
|
| 413 |
+
const W = new Float32Array(k * n);
|
| 414 |
+
for (let i = 0; i < W.length; i++) W[i] = ((i * 17) % 50) / 25 - 1;
|
| 415 |
+
W[200 * n] = 40;
|
| 416 |
+
const col = V.quantizeCols(W, k, n);
|
| 417 |
+
assert.strictEqual(col.s.length, n);
|
| 418 |
+
const blockS = new Float32Array((k / block) * n);
|
| 419 |
+
for (let c = 0; c < n; c++) {
|
| 420 |
+
for (let b = 0; b < k / block; b++) {
|
| 421 |
+
let mx = 0;
|
| 422 |
+
for (let r = b * block; r < (b + 1) * block; r++) mx = Math.max(mx, Math.abs(W[r * n + c]));
|
| 423 |
+
blockS[b * n + c] = Math.max(mx / 127, 1e-8);
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
assert.ok(blockS[0] < col.s[0], "the early block must not inherit a later spike");
|
| 427 |
+
assert.strictEqual(col.s[0], Math.fround(40 / 127));
|
| 428 |
+
assert.strictEqual(V.MAX_INT32_K, 131071);
|
| 429 |
+
});
|
| 430 |
+
|
| 431 |
await t("a non-finite activation is refused before any layer runs", async () => {
|
| 432 |
const S = { w: { layers: [] }, spec: LLAMA, T: 2 };
|
| 433 |
const bad = new Float32Array([0, 1, NaN, 2]);
|