Quazim0t0 commited on
Commit
01ead24
·
1 Parent(s): 0b05d88

Add the 2026-10-23 parameter shard, ring masks, and README notes.

Browse files

A 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 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 # 47 checks: pipeline equivalence, wire protocol, loader
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 resident`;
 
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
- let idsPreview = [...Tokenizer.encode(tok, promptText)];
 
 
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 opts = { temperature: temp, topK: 40, rng: Infer.mulberry32(+ui.seed.value | 0) };
 
 
 
 
 
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.pickToken((await Infer.logitsAt(stageObj, last, [row]))[0], opts);
 
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 picked = (await Infer.logitsAt(stageObj, x, rows)).map(l => Infer.pickToken(l, opts));
 
 
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
- { temperature: temp, topK: 40, rng: Infer.mulberry32(+ui.seed.value | 0) })
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 = 0; j <= see; j++) mx = Math.max(mx, scores[i * N + j] * scale);
187
  let z = 0;
188
- for (let j = 0; j <= see; j++) { const e = Math.exp(scores[i * N + j] * scale - mx); a[i * N + j] = e; z += e; }
189
- for (let j = 0; j <= see; j++) a[i * N + j] /= z;
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
- for (const ly of S.w.layers) x = await layerForward(S, x, ly);
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
- for (let li = 0; li < S.w.layers.length; li++) {
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 = pickToken((await logitsAt(stages[0], last, [row]))[0], opts);
 
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
- let r = (o.rng || Math.random)() * z;
 
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
- for (const logits of await logitsAt(head, x, rows)) picked.push(pickToken(logits, opts));
 
 
 
 
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]);