Spaces:
Running
Running
Release v1.1.3 scored WebGPU evidence
Browse filesAdds browser state-drift scoring and release evidence, refreshes the pinned wllama artifacts, and keeps DFlash inspection-only and disabled by default.
- .gitattributes +2 -0
- README.md +46 -15
- dist/assets/index-DIzN0g8r.js +0 -0
- dist/assets/index-jtL5gUVS.js +0 -0
- dist/assets/{worker-gvajdY5k.js → worker-CUYMhoYj.js} +0 -0
- dist/fixtures/state-drift-27b-cpu-reference.json +1 -0
- dist/index.html +1 -1
- dist/wasm/wllama-compat.wasm +2 -2
- dist/wasm/wllama.wasm +2 -2
- public/fixtures/state-drift-27b-cpu-reference.json +1 -0
- public/wasm/wllama-compat.wasm +2 -2
- public/wasm/wllama.wasm +2 -2
- scripts/verify-wllama-assets.mjs +4 -0
- src/bench/BenchApp.tsx +118 -21
- src/bench/report.test.ts +468 -6
- src/bench/report.ts +444 -20
- src/bench/state-drift-reference.test.ts +81 -0
- src/bench/state-drift-reference.ts +272 -0
- src/engine/client.test.ts +44 -0
- src/engine/client.ts +12 -3
- src/engine/device-gate.test.ts +47 -0
- src/engine/device-gate.ts +41 -0
- src/engine/errors.ts +1 -1
- src/engine/protocol.ts +50 -0
- src/engine/runtime-generate.test.ts +362 -6
- src/engine/runtime-score-sequence.test.ts +225 -0
- src/engine/runtime.ts +449 -9
- src/engine/worker.ts +8 -0
- vendor/wllama-bonsai/PATCH.diff +54 -4
- vendor/wllama-bonsai/SOURCE.json +12 -10
- vendor/wllama-bonsai/esm/index.cjs +0 -0
- vendor/wllama-bonsai/esm/index.js +0 -0
- vendor/wllama-bonsai/esm/index.min.js +0 -0
- vendor/wllama-bonsai/esm/index.min.js.map +0 -0
- vendor/wllama-bonsai/esm/types/oai-compat.d.ts +1 -0
- vendor/wllama-bonsai/esm/workers-code/generated.d.ts +0 -0
.gitattributes
CHANGED
|
@@ -1 +1,3 @@
|
|
| 1 |
public/wasm/*.wasm filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 1 |
public/wasm/*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
dist/wasm/wllama-compat.wasm filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
dist/wasm/wllama.wasm filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -71,8 +71,29 @@ deep-link keeps the benchmark executable under the same COOP/COEP headers. It
|
|
| 71 |
loads nothing until **Run benchmark** is pressed, then records first-load and
|
| 72 |
verified-cache reload wall time, TTFT,
|
| 73 |
prefill/decode throughput, graph placement, device limits, engine revisions,
|
| 74 |
-
the exact runtime artifact hash, and a fixed temperature-0 prompt.
|
| 75 |
-
exports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
`n_batch`, `n_ubatch`, Flash Attention, paired K/V cache types, and the pinned
|
| 78 |
JSPI/compat WASM flavor are available there as explicitly experimental
|
|
@@ -83,14 +104,21 @@ and Q4_0 KV: every row retained one graph, 29/29 GPU layers, zero CPU ops, and
|
|
| 83 |
the same short deterministic output hash. Observed KV allocation fell from
|
| 84 |
112 MiB for F16 to 59.5 MiB for Q8_0 and 31.5 MiB for Q4_0.
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
A same-Chrome A/B with one warmup and three alternating measured runs per arm
|
| 87 |
also proved explicit JSPI/compat switching. Median warm load was 2,257.7 ms for
|
| 88 |
JSPI and 2,799.1 ms for compat; all eight outputs and graph tripwires matched.
|
| 89 |
These are experimental single-device results, not release recommendations;
|
| 90 |
normal chat keeps Flash off, F16/F16, and automatic WASM selection.
|
| 91 |
|
| 92 |
-
The public prewarmed-cache baseline below is one Apple
|
| 93 |
-
Chrome sample, not a cross-device study and not a
|
|
|
|
| 94 |
|
| 95 |
| Tier | Backend | Load | TTFT | Decode |
|
| 96 |
| --- | --- | ---: | ---: | ---: |
|
|
@@ -102,10 +130,11 @@ Chrome sample, not a cross-device study and not a first-download claim:
|
|
| 102 |
## DFlash research lane
|
| 103 |
|
| 104 |
DFlash is visible only as an inspection-only experiment. Its BF16 source and
|
| 105 |
-
Q8_0 native GGUF reference are pinned in the manifest.
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
| 109 |
`fc.weight` also exceeds the 128 MiB baseline WebGPU binding limit. It remains
|
| 110 |
disabled and unavailable until a lower-bit candidate, browser parity,
|
| 111 |
acceptance, and positive speed gates all pass.
|
|
@@ -125,10 +154,10 @@ stay in the browser; only model/static asset downloads contact Hugging Face.
|
|
| 125 |
- Dawn native WebGPU validation: `18eb229ef5f707c1464cc581252e7603c73a3ef0`
|
| 126 |
- custom wllama source: `912c18b75d4358c1405a64646b8dbe43a205943b`
|
| 127 |
- custom nested llama.cpp: `00fa7cb284cbf133fc426733bd64238a3588a33e`
|
| 128 |
-
- custom patch set: `
|
| 129 |
-
- custom ESM: `
|
| 130 |
-
- JSPI WASM: `
|
| 131 |
-
- compat WASM: `
|
| 132 |
- compat worker: `0b667db536815fb9e5dc9ffbfbd6ba29affdda38a4ba892d5372ddb41ce9a8c8`
|
| 133 |
|
| 134 |
The vendored runtime exposes the validated token-embedding WebGPU placement
|
|
@@ -139,9 +168,11 @@ backend tripwire disagrees. Its exact JS/WASM hashes are checked during
|
|
| 139 |
`THIRD_PARTY_NOTICES.md` and `public/licenses/`.
|
| 140 |
|
| 141 |
The pinned WebGPU source implements Flash Attention with Q4_0/Q8_0 K/V. The
|
| 142 |
-
benchmark lane has short deterministic-output
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
| 145 |
|
| 146 |
## Licenses and attribution
|
| 147 |
|
|
|
|
| 71 |
loads nothing until **Run benchmark** is pressed, then records first-load and
|
| 72 |
verified-cache reload wall time, TTFT,
|
| 73 |
prefill/decode throughput, graph placement, device limits, engine revisions,
|
| 74 |
+
the exact runtime artifact hash, and a fixed temperature-0 prompt. Schema v3
|
| 75 |
+
exports the exact messages, sampling controls, raw generated text, pinned model
|
| 76 |
+
source/shard hashes, and an opt-in sampled-token trace. Normal chat does not
|
| 77 |
+
request logprobs and returns no token-id trace.
|
| 78 |
+
|
| 79 |
+
The default `field-core-v1` workload remains short. The separate
|
| 80 |
+
`state-drift-1k-v1` evidence workload locks greedy sampling and refuses to
|
| 81 |
+
export unless all 1,024 sampled token IDs and the 64/128/256/512/768/1,024
|
| 82 |
+
uint32-LE prefix hashes are present. Its diagnostic-only 27B WebGPU path also
|
| 83 |
+
validates the pinned native CPU prompt/reference fixture, then teacher-forces
|
| 84 |
+
that fixed 1,024-token sequence through raw token-ID prefixes. Schema v3 keeps
|
| 85 |
+
the forced reference logprob, natural top five, mean NLL, and perplexity
|
| 86 |
+
separate from the natural browser generation. This scoring method is not
|
| 87 |
+
available to normal chat and does not activate DFlash.
|
| 88 |
+
|
| 89 |
+
On the pinned Apple `metal-3` release path, the final JSPI runtime completed the
|
| 90 |
+
natural 1,024-token 27B run at 11.3 tok/s with one graph, zero CPU ops, and
|
| 91 |
+
65/65 GPU layers. Exact CPU/browser token parity remains `NO-GO` because the
|
| 92 |
+
first natural mismatch is a reciprocal 13/198 top-one/top-two flip at position
|
| 93 |
+
30. The separate numerical gate passes: browser and native CPU mean NLL are
|
| 94 |
+
0.0456380321 and 0.0455182718 (absolute delta 0.0001197604 against a 0.01
|
| 95 |
+
limit), with near-tie margins 0.006506 and 0.003605. This supports a bounded
|
| 96 |
+
GPU floating-point tolerance; it is not a bit-exact parity claim.
|
| 97 |
|
| 98 |
`n_batch`, `n_ubatch`, Flash Attention, paired K/V cache types, and the pinned
|
| 99 |
JSPI/compat WASM flavor are available there as explicitly experimental
|
|
|
|
| 104 |
the same short deterministic output hash. Observed KV allocation fell from
|
| 105 |
112 MiB for F16 to 59.5 MiB for Q8_0 and 31.5 MiB for Q4_0.
|
| 106 |
|
| 107 |
+
The separate final-artifact 27B >8K smoke used an 8,448-token context with
|
| 108 |
+
8,314 prompt tokens, Flash auto, and Q4_0/Q4_0 KV. It measured 7.55 prefill
|
| 109 |
+
tok/s and 6.42 decode tok/s, retained one graph with zero CPU ops and 65/65
|
| 110 |
+
GPU layers, and exported all 8 requested top-five token records. This is one
|
| 111 |
+
local Apple `metal-3` result; it does not establish a cross-device default.
|
| 112 |
+
|
| 113 |
A same-Chrome A/B with one warmup and three alternating measured runs per arm
|
| 114 |
also proved explicit JSPI/compat switching. Median warm load was 2,257.7 ms for
|
| 115 |
JSPI and 2,799.1 ms for compat; all eight outputs and graph tripwires matched.
|
| 116 |
These are experimental single-device results, not release recommendations;
|
| 117 |
normal chat keeps Flash off, F16/F16, and automatic WASM selection.
|
| 118 |
|
| 119 |
+
The historical public v1.1.2 prewarmed-cache baseline below is one Apple
|
| 120 |
+
`metal-3` / headless Chrome sample, not a cross-device study and not a
|
| 121 |
+
first-download claim:
|
| 122 |
|
| 123 |
| Tier | Backend | Load | TTFT | Decode |
|
| 124 |
| --- | --- | ---: | ---: | ---: |
|
|
|
|
| 130 |
## DFlash research lane
|
| 131 |
|
| 132 |
DFlash is visible only as an inspection-only experiment. Its BF16 source and
|
| 133 |
+
Q8_0 native GGUF reference are pinned in the manifest. The strict D1 audit
|
| 134 |
+
passes all 39 shape, tokenizer, target-tap, conversion, revision, and evidence
|
| 135 |
+
checks, including a 32-token temperature-0 native Bonsai-27B parity smoke. The
|
| 136 |
+
custom WASM contains the DFlash llama.cpp architecture, but the browser load
|
| 137 |
+
API does not yet plumb `spec_type=draft-dflash`; the Q8
|
| 138 |
`fc.weight` also exceeds the 128 MiB baseline WebGPU binding limit. It remains
|
| 139 |
disabled and unavailable until a lower-bit candidate, browser parity,
|
| 140 |
acceptance, and positive speed gates all pass.
|
|
|
|
| 154 |
- Dawn native WebGPU validation: `18eb229ef5f707c1464cc581252e7603c73a3ef0`
|
| 155 |
- custom wllama source: `912c18b75d4358c1405a64646b8dbe43a205943b`
|
| 156 |
- custom nested llama.cpp: `00fa7cb284cbf133fc426733bd64238a3588a33e`
|
| 157 |
+
- custom patch set: `27b96fa48c6fd66b44fe70e83cc775902e29ec5877261c7eb04fecabc529d2f9`
|
| 158 |
+
- custom ESM: `10c3811776e34092a225632929b97046af4215a4c91156169105c886be5cba5d`
|
| 159 |
+
- JSPI WASM: `b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a`
|
| 160 |
+
- compat WASM: `ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c`
|
| 161 |
- compat worker: `0b667db536815fb9e5dc9ffbfbd6ba29affdda38a4ba892d5372ddb41ce9a8c8`
|
| 162 |
|
| 163 |
The vendored runtime exposes the validated token-embedding WebGPU placement
|
|
|
|
| 168 |
`THIRD_PARTY_NOTICES.md` and `public/licenses/`.
|
| 169 |
|
| 170 |
The pinned WebGPU source implements Flash Attention with Q4_0/Q8_0 K/V. The
|
| 171 |
+
benchmark lane has short 1.7B deterministic-output and memory evidence for
|
| 172 |
+
both quantized types, a 27B long-state result only for Flash-off F16, and a
|
| 173 |
+
27B >8K smoke only for Flash-auto Q4_0. No 27B Q8_0 long-state result is
|
| 174 |
+
claimed. Repeated and cross-device gates remain open, so Flash Attention
|
| 175 |
+
remains off and F16/F16 remains the release default.
|
| 176 |
|
| 177 |
## Licenses and attribution
|
| 178 |
|
dist/assets/index-DIzN0g8r.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/index-jtL5gUVS.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/{worker-gvajdY5k.js → worker-CUYMhoYj.js}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/fixtures/state-drift-27b-cpu-reference.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"schemaVersion":1,"kind":"bonsai-state-drift-reference","workload":{"id":"state-drift-1k-v1","messages":[{"role":"user","content":"Output consecutive integers starting at 1, separated only by a comma and one space. Continue without explanation until you reach 500."}]},"provenance":{"sourceEvidence":"results/space-model/27b-native-cpu-state-drift-1024-b32-u16.json","engineRevision":"00fa7cb284cbf133fc426733bd64238a3588a33e","nativeBinary":{"bytes":65392,"sha256":"e8f23d8612ed27739f21a6aca1df618699da902f6fc42cbdb3f9c978f6da04fb"},"model":{"file":"Bonsai-27B-Q1_0.gguf","bytes":3803452480,"sha256":"17ef842e47450caeb8eaa3ebfbbab5d2f2278b62b79be107985fb69a2f819aa0"},"renderedPromptSha256":"7c46d0de443e6f1235ddaa7c0a55c9da710eb73958d5671247b6f0e3b7d189c7","execution":{"backend":"cpu","contextSize":2048,"batchSize":32,"microBatchSize":16}},"tokenEncoding":"uint32-le","promptTokenIds":[248045,846,198,4935,23173,24959,5732,506,220,16,11,18101,1132,539,264,30644,321,799,3433,13,14569,1973,15673,2980,488,5372,220,20,15,15,13,248046,198,248045,74455,198,248068,198],"promptTokenIdsSha256":"e7af7d66c96f24cb148575bd45cef079920669eb0d193fdc830ce61dfec85451","referenceTokenIds":[8160,579,264,7047,1817,25,271,16,13,220,2972,2014,53983,2570,5396,64700,198,256,471,2972,6065,64700,8984,23173,24959,5732,506,220,16,13,198,256,471,2972,3925,64700,32001,639,1132,539,264,30644,321,799,3433,318,72,1673,2487,328,16,11,220,17,11,220,18,11,2423,1764,198,256,471,2972,17355,64700,14569,1973,15673,2980,18176,220,20,15,15,13,198,256,471,2972,57545,64700,561,2468,1220,381,264,3074,18677,886,20296,314,4947,494,220,16,310,220,20,15,15,11,22405,6681,430,5024,13,271,17,13,220,2972,27382,1386,5141,84457,64700,198,256,471,4980,506,220,16,198,256,471,3839,506,220,20,15,15,198,256,471,75604,25,3544,328,318,43871,478,3433,8,198,256,471,2233,39492,11,1066,279,4947,198,256,471,1160,82819,24959,271,18,13,220,2972,35,23544,8984,14606,64700,198,256,561,2468,1220,1353,1040,25,198,256,1510,16,11,220,17,11,220,18,11,220,19,11,220,20,11,58317,220,19,24,24,11,220,20,15,15,63,271,19,13,220,2972,30097,279,27929,64700,198,256,353,1144,310,6707,4947,494,220,16,310,220,20,15,15,11,10542,539,3544,5748,198,256,8439,353,2688,449,14791,11,353,628,6707,411,53624,6804,303,821,1965,13,353,3172,8972,424,15060,13,271,256,6558,579,9845,279,4581,3443,25,328,16,11,220,17,11,220,18,11,58317,220,20,15,15,1,198,256,353,3172,6707,424,5774,13,271,256,6817,25,353,1144,310,1236,2617,1017,579,874,4799,1414,11,874,48794,35358,421,2493,38556,11,1066,279,6891,1414,430,10897,13,561,9640,2640,328,323,47930,1132,539,264,30644,321,799,3433,487,748,353,3172,24660,1732,421,13,271,256,353,3172,6707,279,2400,8240,1381,13,271,256,27929,25,220,16,11,220,17,11,220,18,11,220,19,11,220,20,11,220,21,11,220,22,11,220,23,11,220,24,11,220,16,15,11,220,16,16,11,220,16,17,11,220,16,18,11,220,16,19,11,220,16,20,11,220,16,21,11,220,16,22,11,220,16,23,11,220,16,24,11,220,17,15,11,220,17,16,11,220,17,17,11,220,17,18,11,220,17,19,11,220,17,20,11,220,17,21,11,220,17,22,11,220,17,23,11,220,17,24,11,220,18,15,11,220,18,16,11,220,18,17,11,220,18,18,11,220,18,19,11,220,18,20,11,220,18,21,11,220,18,22,11,220,18,23,11,220,18,24,11,220,19,15,11,220,19,16,11,220,19,17,11,220,19,18,11,220,19,19,11,220,19,20,11,220,19,21,11,220,19,22,11,220,19,23,11,220,19,24,11,220,20,15,11,220,20,16,11,220,20,17,11,220,20,18,11,220,20,19,11,220,20,20,11,220,20,21,11,220,20,22,11,220,20,23,11,220,20,24,11,220,21,15,11,220,21,16,11,220,21,17,11,220,21,18,11,220,21,19,11,220,21,20,11,220,21,21,11,220,21,22,11,220,21,23,11,220,21,24,11,220,22,15,11,220,22,16,11,220,22,17,11,220,22,18,11,220,22,19,11,220,22,20,11,220,22,21,11,220,22,22,11,220,22,23,11,220,22,24,11,220,23,15,11,220,23,16,11,220,23,17,11,220,23,18,11,220,23,19,11,220,23,20,11,220,23,21,11,220,23,22,11,220,23,23,11,220,23,24,11,220,24,15,11,220,24,16,11,220,24,17,11,220,24,18,11,220,24,19,11,220,24,20,11,220,24,21,11,220,24,22,11,220,24,23,11,220,24,24,11,220,16,15,15,11,220,16,15,16,11,220,16,15,17,11,220,16,15,18,11,220,16,15,19,11,220,16,15,20,11,220,16,15,21,11,220,16,15,22,11,220,16,15,23,11,220,16,15,24,11,220,16,16,15,11,220,16,16,16,11,220,16,16,17,11,220,16,16,18,11,220,16,16,19,11,220,16,16,20,11,220,16,16,21,11,220,16,16,22,11,220,16,16,23,11,220,16,16,24,11,220,16,17,15,11,220,16,17,16,11,220,16,17,17,11,220,16,17,18,11,220,16,17,19,11,220,16,17,20,11,220,16,17,21,11,220,16,17,22,11,220,16,17,23,11,220,16,17,24,11,220,16,18,15,11,220,16,18,16,11,220,16,18,17,11,220,16,18,18,11,220,16,18,19,11,220,16,18,20,11,220,16,18,21,11,220,16,18,22,11,220,16,18,23,11,220,16,18,24,11,220,16,19,15,11,220,16,19,16,11,220,16,19,17,11,220,16,19,18,11,220,16,19,19,11,220,16,19,20,11,220,16,19,21,11,220,16,19,22,11,220,16,19,23,11,220,16,19,24,11,220,16,20,15,11,220,16,20,16,11,220,16,20,17,11,220,16,20,18,11,220,16,20,19,11,220,16,20,20,11,220,16],"referenceTokenIdsSha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b","checkpointPrefixes":[{"tokens":64,"sha256":"f6398ce3b2c935435615b8ba33d0416c03228e4a1ab0a9ff3f4229418558a130"},{"tokens":128,"sha256":"0efb5a50141c6b4140846e69df0e3ed3b86491040fa52f3cc4fb1ab561c153a5"},{"tokens":256,"sha256":"f4e94ebe35a375348e5156732df7fc858060cd350b4e2f366e6c249758db7b9e"},{"tokens":512,"sha256":"11acf787b85658d6a8db7a4d872faf9145684291aba790b39afd184ecf48e7b9"},{"tokens":768,"sha256":"faa10cea44ae4d461f355ed31f1599b5b67c6ac121b9296c2ed24af8af92aec7"},{"tokens":1024,"sha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b"}]}
|
dist/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
content="Bonsai local inference field station — private WebGPU language models in your browser."
|
| 11 |
/>
|
| 12 |
<title>Bonsai Field Station</title>
|
| 13 |
-
<script type="module" crossorigin src="/assets/index-
|
| 14 |
<link rel="stylesheet" crossorigin href="/assets/index-C-2tm91j.css">
|
| 15 |
</head>
|
| 16 |
<body>
|
|
|
|
| 10 |
content="Bonsai local inference field station — private WebGPU language models in your browser."
|
| 11 |
/>
|
| 12 |
<title>Bonsai Field Station</title>
|
| 13 |
+
<script type="module" crossorigin src="/assets/index-DIzN0g8r.js"></script>
|
| 14 |
<link rel="stylesheet" crossorigin href="/assets/index-C-2tm91j.css">
|
| 15 |
</head>
|
| 16 |
<body>
|
dist/wasm/wllama-compat.wasm
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c
|
| 3 |
+
size 14865338
|
dist/wasm/wllama.wasm
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a
|
| 3 |
+
size 8017784
|
public/fixtures/state-drift-27b-cpu-reference.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"schemaVersion":1,"kind":"bonsai-state-drift-reference","workload":{"id":"state-drift-1k-v1","messages":[{"role":"user","content":"Output consecutive integers starting at 1, separated only by a comma and one space. Continue without explanation until you reach 500."}]},"provenance":{"sourceEvidence":"results/space-model/27b-native-cpu-state-drift-1024-b32-u16.json","engineRevision":"00fa7cb284cbf133fc426733bd64238a3588a33e","nativeBinary":{"bytes":65392,"sha256":"e8f23d8612ed27739f21a6aca1df618699da902f6fc42cbdb3f9c978f6da04fb"},"model":{"file":"Bonsai-27B-Q1_0.gguf","bytes":3803452480,"sha256":"17ef842e47450caeb8eaa3ebfbbab5d2f2278b62b79be107985fb69a2f819aa0"},"renderedPromptSha256":"7c46d0de443e6f1235ddaa7c0a55c9da710eb73958d5671247b6f0e3b7d189c7","execution":{"backend":"cpu","contextSize":2048,"batchSize":32,"microBatchSize":16}},"tokenEncoding":"uint32-le","promptTokenIds":[248045,846,198,4935,23173,24959,5732,506,220,16,11,18101,1132,539,264,30644,321,799,3433,13,14569,1973,15673,2980,488,5372,220,20,15,15,13,248046,198,248045,74455,198,248068,198],"promptTokenIdsSha256":"e7af7d66c96f24cb148575bd45cef079920669eb0d193fdc830ce61dfec85451","referenceTokenIds":[8160,579,264,7047,1817,25,271,16,13,220,2972,2014,53983,2570,5396,64700,198,256,471,2972,6065,64700,8984,23173,24959,5732,506,220,16,13,198,256,471,2972,3925,64700,32001,639,1132,539,264,30644,321,799,3433,318,72,1673,2487,328,16,11,220,17,11,220,18,11,2423,1764,198,256,471,2972,17355,64700,14569,1973,15673,2980,18176,220,20,15,15,13,198,256,471,2972,57545,64700,561,2468,1220,381,264,3074,18677,886,20296,314,4947,494,220,16,310,220,20,15,15,11,22405,6681,430,5024,13,271,17,13,220,2972,27382,1386,5141,84457,64700,198,256,471,4980,506,220,16,198,256,471,3839,506,220,20,15,15,198,256,471,75604,25,3544,328,318,43871,478,3433,8,198,256,471,2233,39492,11,1066,279,4947,198,256,471,1160,82819,24959,271,18,13,220,2972,35,23544,8984,14606,64700,198,256,561,2468,1220,1353,1040,25,198,256,1510,16,11,220,17,11,220,18,11,220,19,11,220,20,11,58317,220,19,24,24,11,220,20,15,15,63,271,19,13,220,2972,30097,279,27929,64700,198,256,353,1144,310,6707,4947,494,220,16,310,220,20,15,15,11,10542,539,3544,5748,198,256,8439,353,2688,449,14791,11,353,628,6707,411,53624,6804,303,821,1965,13,353,3172,8972,424,15060,13,271,256,6558,579,9845,279,4581,3443,25,328,16,11,220,17,11,220,18,11,58317,220,20,15,15,1,198,256,353,3172,6707,424,5774,13,271,256,6817,25,353,1144,310,1236,2617,1017,579,874,4799,1414,11,874,48794,35358,421,2493,38556,11,1066,279,6891,1414,430,10897,13,561,9640,2640,328,323,47930,1132,539,264,30644,321,799,3433,487,748,353,3172,24660,1732,421,13,271,256,353,3172,6707,279,2400,8240,1381,13,271,256,27929,25,220,16,11,220,17,11,220,18,11,220,19,11,220,20,11,220,21,11,220,22,11,220,23,11,220,24,11,220,16,15,11,220,16,16,11,220,16,17,11,220,16,18,11,220,16,19,11,220,16,20,11,220,16,21,11,220,16,22,11,220,16,23,11,220,16,24,11,220,17,15,11,220,17,16,11,220,17,17,11,220,17,18,11,220,17,19,11,220,17,20,11,220,17,21,11,220,17,22,11,220,17,23,11,220,17,24,11,220,18,15,11,220,18,16,11,220,18,17,11,220,18,18,11,220,18,19,11,220,18,20,11,220,18,21,11,220,18,22,11,220,18,23,11,220,18,24,11,220,19,15,11,220,19,16,11,220,19,17,11,220,19,18,11,220,19,19,11,220,19,20,11,220,19,21,11,220,19,22,11,220,19,23,11,220,19,24,11,220,20,15,11,220,20,16,11,220,20,17,11,220,20,18,11,220,20,19,11,220,20,20,11,220,20,21,11,220,20,22,11,220,20,23,11,220,20,24,11,220,21,15,11,220,21,16,11,220,21,17,11,220,21,18,11,220,21,19,11,220,21,20,11,220,21,21,11,220,21,22,11,220,21,23,11,220,21,24,11,220,22,15,11,220,22,16,11,220,22,17,11,220,22,18,11,220,22,19,11,220,22,20,11,220,22,21,11,220,22,22,11,220,22,23,11,220,22,24,11,220,23,15,11,220,23,16,11,220,23,17,11,220,23,18,11,220,23,19,11,220,23,20,11,220,23,21,11,220,23,22,11,220,23,23,11,220,23,24,11,220,24,15,11,220,24,16,11,220,24,17,11,220,24,18,11,220,24,19,11,220,24,20,11,220,24,21,11,220,24,22,11,220,24,23,11,220,24,24,11,220,16,15,15,11,220,16,15,16,11,220,16,15,17,11,220,16,15,18,11,220,16,15,19,11,220,16,15,20,11,220,16,15,21,11,220,16,15,22,11,220,16,15,23,11,220,16,15,24,11,220,16,16,15,11,220,16,16,16,11,220,16,16,17,11,220,16,16,18,11,220,16,16,19,11,220,16,16,20,11,220,16,16,21,11,220,16,16,22,11,220,16,16,23,11,220,16,16,24,11,220,16,17,15,11,220,16,17,16,11,220,16,17,17,11,220,16,17,18,11,220,16,17,19,11,220,16,17,20,11,220,16,17,21,11,220,16,17,22,11,220,16,17,23,11,220,16,17,24,11,220,16,18,15,11,220,16,18,16,11,220,16,18,17,11,220,16,18,18,11,220,16,18,19,11,220,16,18,20,11,220,16,18,21,11,220,16,18,22,11,220,16,18,23,11,220,16,18,24,11,220,16,19,15,11,220,16,19,16,11,220,16,19,17,11,220,16,19,18,11,220,16,19,19,11,220,16,19,20,11,220,16,19,21,11,220,16,19,22,11,220,16,19,23,11,220,16,19,24,11,220,16,20,15,11,220,16,20,16,11,220,16,20,17,11,220,16,20,18,11,220,16,20,19,11,220,16,20,20,11,220,16],"referenceTokenIdsSha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b","checkpointPrefixes":[{"tokens":64,"sha256":"f6398ce3b2c935435615b8ba33d0416c03228e4a1ab0a9ff3f4229418558a130"},{"tokens":128,"sha256":"0efb5a50141c6b4140846e69df0e3ed3b86491040fa52f3cc4fb1ab561c153a5"},{"tokens":256,"sha256":"f4e94ebe35a375348e5156732df7fc858060cd350b4e2f366e6c249758db7b9e"},{"tokens":512,"sha256":"11acf787b85658d6a8db7a4d872faf9145684291aba790b39afd184ecf48e7b9"},{"tokens":768,"sha256":"faa10cea44ae4d461f355ed31f1599b5b67c6ac121b9296c2ed24af8af92aec7"},{"tokens":1024,"sha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b"}]}
|
public/wasm/wllama-compat.wasm
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c
|
| 3 |
+
size 14865338
|
public/wasm/wllama.wasm
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a
|
| 3 |
+
size 8017784
|
scripts/verify-wllama-assets.mjs
CHANGED
|
@@ -44,6 +44,10 @@ const wllamaDeclarations = await readFile(resolve(root, 'vendor/wllama-bonsai/es
|
|
| 44 |
if (!wllamaDeclarations.includes('forceCompat?: boolean')) {
|
| 45 |
throw new Error('Vendored Bonsai wllama declarations lack forceCompat');
|
| 46 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
const moduleSource = await readFile(resolve(root, 'vendor/wllama-bonsai/esm/index.js'), 'utf8');
|
| 48 |
if (!moduleSource.includes('n_ubatch: params.n_ubatch')) {
|
| 49 |
throw new Error('Vendored Bonsai wllama module does not forward n_ubatch');
|
|
|
|
| 44 |
if (!wllamaDeclarations.includes('forceCompat?: boolean')) {
|
| 45 |
throw new Error('Vendored Bonsai wllama declarations lack forceCompat');
|
| 46 |
}
|
| 47 |
+
const oaiDeclarations = await readFile(resolve(root, 'vendor/wllama-bonsai/esm/types/oai-compat.d.ts'), 'utf8');
|
| 48 |
+
if (!oaiDeclarations.includes('export interface ChatCompletionLogprob {\n id: number;')) {
|
| 49 |
+
throw new Error('Vendored Bonsai wllama declarations lack sampled token ids');
|
| 50 |
+
}
|
| 51 |
const moduleSource = await readFile(resolve(root, 'vendor/wllama-bonsai/esm/index.js'), 'utf8');
|
| 52 |
if (!moduleSource.includes('n_ubatch: params.n_ubatch')) {
|
| 53 |
throw new Error('Vendored Bonsai wllama module does not forward n_ubatch');
|
src/bench/BenchApp.tsx
CHANGED
|
@@ -2,25 +2,33 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
| 2 |
import {
|
| 3 |
BrowserEngineClient,
|
| 4 |
EngineClientError,
|
|
|
|
| 5 |
evaluateModelGate,
|
| 6 |
loadModelManifestV2,
|
| 7 |
type BenchmarkFlashMode,
|
| 8 |
type BenchmarkKvCacheType,
|
| 9 |
type BenchmarkWasmFlavor,
|
| 10 |
type EngineCapabilities,
|
|
|
|
| 11 |
type ModelManifestV2,
|
| 12 |
type ModelTierId,
|
| 13 |
type RequestedBackend,
|
| 14 |
} from '../engine';
|
| 15 |
import { formatBytes } from '../lib/format';
|
| 16 |
import {
|
| 17 |
-
|
| 18 |
-
|
| 19 |
benchmarkReportFilename,
|
| 20 |
buildBenchmarkReport,
|
| 21 |
serializeBenchmarkReport,
|
| 22 |
type BenchmarkReport,
|
|
|
|
|
|
|
| 23 |
} from './report';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
const MANIFEST_PATH = 'manifest/models.json';
|
| 26 |
const DEFAULT_MODEL: ModelTierId = '1_7b';
|
|
@@ -43,6 +51,13 @@ function manifestUrl(): string {
|
|
| 43 |
return new URL(`${import.meta.env.BASE_URL}${MANIFEST_PATH}`, document.baseURI).href;
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
function errorMessage(error: unknown): string {
|
| 47 |
if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
|
| 48 |
return error instanceof Error ? error.message : String(error);
|
|
@@ -89,6 +104,7 @@ export function BenchApp() {
|
|
| 89 |
const [client] = useState(getBenchClient);
|
| 90 |
const [manifest, setManifest] = useState<ModelManifestV2 | null>(null);
|
| 91 |
const [capabilities, setCapabilities] = useState<EngineCapabilities | null>(null);
|
|
|
|
| 92 |
const [modelId, setModelId] = useState<ModelTierId>(DEFAULT_MODEL);
|
| 93 |
const [backend, setBackend] = useState<RequestedBackend>('auto');
|
| 94 |
const [contextSize, setContextSize] = useState(4_096);
|
|
@@ -146,6 +162,7 @@ export function BenchApp() {
|
|
| 146 |
() => manifest?.models.find((candidate) => candidate.id === modelId) ?? null,
|
| 147 |
[manifest, modelId],
|
| 148 |
);
|
|
|
|
| 149 |
const deviceGate = useMemo(() => {
|
| 150 |
if (!model || !capabilities) return null;
|
| 151 |
return evaluateModelGate(model, backend, capabilities.webgpu, {
|
|
@@ -159,9 +176,15 @@ export function BenchApp() {
|
|
| 159 |
return evaluateModelGate(model, backend, capabilities.webgpu, capabilities.storage);
|
| 160 |
}, [backend, capabilities, model]);
|
| 161 |
|
| 162 |
-
const
|
| 163 |
-
? model
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
const nBatch = parseOptionalInteger(nBatchInput);
|
| 166 |
const nUbatch = parseOptionalInteger(nUbatchInput);
|
| 167 |
const batchTuningError = nBatchInput.trim() !== '' && nBatch === null
|
|
@@ -179,13 +202,27 @@ export function BenchApp() {
|
|
| 179 |
const wasmTuningError = wasmFlavor === 'jspi' && capabilities?.runtime.wasmFlavor === 'compat'
|
| 180 |
? 'JSPI is unavailable in this browser; use auto or compat.'
|
| 181 |
: null;
|
| 182 |
-
const
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
&& Number.isSafeInteger(maxTokens)
|
| 187 |
&& maxTokens >= 8
|
| 188 |
-
&& maxTokens <=
|
| 189 |
&& configurationTuningError === null;
|
| 190 |
const canRun = Boolean(manifest && capabilities && model && deviceGate?.allowed && configurationValid);
|
| 191 |
|
|
@@ -299,9 +336,8 @@ export function BenchApp() {
|
|
| 299 |
let firstTokenAt: number | null = null;
|
| 300 |
let streamedTokenEvents = 0;
|
| 301 |
const generationStarted = performance.now();
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
messages: [{ role: 'user', content: BENCHMARK_PROMPT }],
|
| 305 |
maxTokens,
|
| 306 |
temperature: 0,
|
| 307 |
topK: 1,
|
|
@@ -309,7 +345,10 @@ export function BenchApp() {
|
|
| 309 |
seed: 42,
|
| 310 |
toolChoice: 'none',
|
| 311 |
cachePrompt: false,
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
| 313 |
signal: controller.signal,
|
| 314 |
onToken: (text, reasoningDelta) => {
|
| 315 |
if (!text && !reasoningDelta) return;
|
|
@@ -324,6 +363,40 @@ export function BenchApp() {
|
|
| 324 |
});
|
| 325 |
const generationCompleted = performance.now();
|
| 326 |
throwIfAborted(controller.signal);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
const backendReport = await client.backendReport();
|
| 328 |
throwIfAborted(controller.signal);
|
| 329 |
|
|
@@ -335,7 +408,8 @@ export function BenchApp() {
|
|
| 335 |
capabilities,
|
| 336 |
requestedBackend: backend,
|
| 337 |
contextSize: warmLoad.context.size,
|
| 338 |
-
|
|
|
|
| 339 |
coldLoadMs,
|
| 340 |
warmLoadMs,
|
| 341 |
coldCachedBytes,
|
|
@@ -346,9 +420,10 @@ export function BenchApp() {
|
|
| 346 |
timeToFirstTokenMs: firstTokenAt === null ? null : firstTokenAt - generationStarted,
|
| 347 |
streamedTokenEvents,
|
| 348 |
backendReport,
|
|
|
|
| 349 |
});
|
| 350 |
setReport(nextReport);
|
| 351 |
-
setOutput(generationResult.text);
|
| 352 |
setProgress({
|
| 353 |
label: 'Benchmark complete · JSON report is ready',
|
| 354 |
loadedBytes: 1,
|
|
@@ -398,6 +473,26 @@ export function BenchApp() {
|
|
| 398 |
<p className="bench-control-intro">The fixed prompt and temperature stay locked so exported runs remain comparable.</p>
|
| 399 |
|
| 400 |
<form onSubmit={(event) => { event.preventDefault(); runBenchmark(); }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
<label className="bench-field">
|
| 402 |
<span>Model tier</span>
|
| 403 |
<select
|
|
@@ -451,7 +546,7 @@ export function BenchApp() {
|
|
| 451 |
<input
|
| 452 |
type="number"
|
| 453 |
min="8"
|
| 454 |
-
max="
|
| 455 |
step="8"
|
| 456 |
value={maxTokens}
|
| 457 |
disabled={running}
|
|
@@ -460,7 +555,7 @@ export function BenchApp() {
|
|
| 460 |
clearPreviousRun();
|
| 461 |
}}
|
| 462 |
/>
|
| 463 |
-
<small>8–
|
| 464 |
</label>
|
| 465 |
</div>
|
| 466 |
|
|
@@ -642,6 +737,8 @@ export function BenchApp() {
|
|
| 642 |
<div><dt>Decode</dt><dd>{formatRate(report?.generation.decodeTokensPerSecond)}</dd></div>
|
| 643 |
<div><dt>Token count</dt><dd>{report?.generation.completionTokens ?? '—'}</dd></div>
|
| 644 |
<div><dt>Finish</dt><dd>{report?.generation.finishReason ?? '—'}</dd></div>
|
|
|
|
|
|
|
| 645 |
</dl>
|
| 646 |
</article>
|
| 647 |
<article className="bench-data-card">
|
|
@@ -677,11 +774,11 @@ export function BenchApp() {
|
|
| 677 |
<section className="bench-output">
|
| 678 |
<div>
|
| 679 |
<p className="eyebrow ink">Fixed specimen</p>
|
| 680 |
-
<h3>{
|
| 681 |
-
<p>{
|
| 682 |
</div>
|
| 683 |
<div>
|
| 684 |
-
<p className="eyebrow ink">Generated
|
| 685 |
<p data-testid="bench-generated-output">{output || 'Run the assay to inspect the generated specimen.'}</p>
|
| 686 |
</div>
|
| 687 |
</section>
|
|
|
|
| 2 |
import {
|
| 3 |
BrowserEngineClient,
|
| 4 |
EngineClientError,
|
| 5 |
+
evaluateModelContextPolicy,
|
| 6 |
evaluateModelGate,
|
| 7 |
loadModelManifestV2,
|
| 8 |
type BenchmarkFlashMode,
|
| 9 |
type BenchmarkKvCacheType,
|
| 10 |
type BenchmarkWasmFlavor,
|
| 11 |
type EngineCapabilities,
|
| 12 |
+
type GenerateParams,
|
| 13 |
type ModelManifestV2,
|
| 14 |
type ModelTierId,
|
| 15 |
type RequestedBackend,
|
| 16 |
} from '../engine';
|
| 17 |
import { formatBytes } from '../lib/format';
|
| 18 |
import {
|
| 19 |
+
BENCHMARK_WORKLOADS,
|
| 20 |
+
DEFAULT_BENCHMARK_WORKLOAD_ID,
|
| 21 |
benchmarkReportFilename,
|
| 22 |
buildBenchmarkReport,
|
| 23 |
serializeBenchmarkReport,
|
| 24 |
type BenchmarkReport,
|
| 25 |
+
type BenchmarkWorkloadId,
|
| 26 |
+
type TeacherForcedReferenceScoreEvidence,
|
| 27 |
} from './report';
|
| 28 |
+
import {
|
| 29 |
+
STATE_DRIFT_REFERENCE_PATH,
|
| 30 |
+
validateStateDriftReferenceFixture,
|
| 31 |
+
} from './state-drift-reference';
|
| 32 |
|
| 33 |
const MANIFEST_PATH = 'manifest/models.json';
|
| 34 |
const DEFAULT_MODEL: ModelTierId = '1_7b';
|
|
|
|
| 51 |
return new URL(`${import.meta.env.BASE_URL}${MANIFEST_PATH}`, document.baseURI).href;
|
| 52 |
}
|
| 53 |
|
| 54 |
+
function stateDriftReferenceUrl(): string {
|
| 55 |
+
return new URL(
|
| 56 |
+
`${import.meta.env.BASE_URL}${STATE_DRIFT_REFERENCE_PATH}`,
|
| 57 |
+
document.baseURI,
|
| 58 |
+
).href;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
function errorMessage(error: unknown): string {
|
| 62 |
if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
|
| 63 |
return error instanceof Error ? error.message : String(error);
|
|
|
|
| 104 |
const [client] = useState(getBenchClient);
|
| 105 |
const [manifest, setManifest] = useState<ModelManifestV2 | null>(null);
|
| 106 |
const [capabilities, setCapabilities] = useState<EngineCapabilities | null>(null);
|
| 107 |
+
const [workloadId, setWorkloadId] = useState<BenchmarkWorkloadId>(DEFAULT_BENCHMARK_WORKLOAD_ID);
|
| 108 |
const [modelId, setModelId] = useState<ModelTierId>(DEFAULT_MODEL);
|
| 109 |
const [backend, setBackend] = useState<RequestedBackend>('auto');
|
| 110 |
const [contextSize, setContextSize] = useState(4_096);
|
|
|
|
| 162 |
() => manifest?.models.find((candidate) => candidate.id === modelId) ?? null,
|
| 163 |
[manifest, modelId],
|
| 164 |
);
|
| 165 |
+
const workload = BENCHMARK_WORKLOADS[workloadId];
|
| 166 |
const deviceGate = useMemo(() => {
|
| 167 |
if (!model || !capabilities) return null;
|
| 168 |
return evaluateModelGate(model, backend, capabilities.webgpu, {
|
|
|
|
| 176 |
return evaluateModelGate(model, backend, capabilities.webgpu, capabilities.storage);
|
| 177 |
}, [backend, capabilities, model]);
|
| 178 |
|
| 179 |
+
const contextPolicy = model
|
| 180 |
+
? evaluateModelContextPolicy(model, contextSize, {
|
| 181 |
+
tuningScope: 'benchmark',
|
| 182 |
+
requestedBackend: backend,
|
| 183 |
+
flashMode,
|
| 184 |
+
kvCacheType,
|
| 185 |
+
})
|
| 186 |
+
: null;
|
| 187 |
+
const contextLimit = contextPolicy?.limit ?? 4_096;
|
| 188 |
const nBatch = parseOptionalInteger(nBatchInput);
|
| 189 |
const nUbatch = parseOptionalInteger(nUbatchInput);
|
| 190 |
const batchTuningError = nBatchInput.trim() !== '' && nBatch === null
|
|
|
|
| 202 |
const wasmTuningError = wasmFlavor === 'jspi' && capabilities?.runtime.wasmFlavor === 'compat'
|
| 203 |
? 'JSPI is unavailable in this browser; use auto or compat.'
|
| 204 |
: null;
|
| 205 |
+
const workloadError = workload.requiredMaxTokens !== null && maxTokens !== workload.requiredMaxTokens
|
| 206 |
+
? `${workload.id} requires exactly ${workload.requiredMaxTokens.toLocaleString()} max tokens.`
|
| 207 |
+
: null;
|
| 208 |
+
const stateDriftReferenceError = workload.id === 'state-drift-1k-v1' && (
|
| 209 |
+
model?.id !== '27b'
|
| 210 |
+
|| backend !== 'webgpu'
|
| 211 |
+
|| contextSize !== 2_048
|
| 212 |
+
|| nBatch !== 32
|
| 213 |
+
|| nUbatch !== 16
|
| 214 |
+
)
|
| 215 |
+
? 'state-drift-1k-v1 reference scoring requires 27B, explicit WebGPU, context 2,048, nBatch 32, and nUbatch 16.'
|
| 216 |
+
: null;
|
| 217 |
+
const configurationTuningError = batchTuningError
|
| 218 |
+
?? runtimeTuningError
|
| 219 |
+
?? wasmTuningError
|
| 220 |
+
?? workloadError
|
| 221 |
+
?? stateDriftReferenceError;
|
| 222 |
+
const configurationValid = contextPolicy?.allowed === true
|
| 223 |
&& Number.isSafeInteger(maxTokens)
|
| 224 |
&& maxTokens >= 8
|
| 225 |
+
&& maxTokens <= 1_024
|
| 226 |
&& configurationTuningError === null;
|
| 227 |
const canRun = Boolean(manifest && capabilities && model && deviceGate?.allowed && configurationValid);
|
| 228 |
|
|
|
|
| 336 |
let firstTokenAt: number | null = null;
|
| 337 |
let streamedTokenEvents = 0;
|
| 338 |
const generationStarted = performance.now();
|
| 339 |
+
const generationRequest = {
|
| 340 |
+
messages: workload.messages.map((message) => ({ ...message })),
|
|
|
|
| 341 |
maxTokens,
|
| 342 |
temperature: 0,
|
| 343 |
topK: 1,
|
|
|
|
| 345 |
seed: 42,
|
| 346 |
toolChoice: 'none',
|
| 347 |
cachePrompt: false,
|
| 348 |
+
returnTokenIds: true,
|
| 349 |
+
} satisfies GenerateParams;
|
| 350 |
+
setProgress({ label: `Generating fixed workload · ${workload.id}`, loadedBytes: 0, totalBytes: maxTokens });
|
| 351 |
+
const generationResult = await client.generate(generationRequest, {
|
| 352 |
signal: controller.signal,
|
| 353 |
onToken: (text, reasoningDelta) => {
|
| 354 |
if (!text && !reasoningDelta) return;
|
|
|
|
| 363 |
});
|
| 364 |
const generationCompleted = performance.now();
|
| 365 |
throwIfAborted(controller.signal);
|
| 366 |
+
let teacherForcedReferenceScore: TeacherForcedReferenceScoreEvidence | null = null;
|
| 367 |
+
if (workload.id === 'state-drift-1k-v1') {
|
| 368 |
+
setProgress({
|
| 369 |
+
label: 'Loading and validating pinned CPU reference',
|
| 370 |
+
loadedBytes: 0,
|
| 371 |
+
totalBytes: 1,
|
| 372 |
+
});
|
| 373 |
+
const fixtureResponse = await fetch(stateDriftReferenceUrl(), {
|
| 374 |
+
signal: controller.signal,
|
| 375 |
+
cache: 'no-cache',
|
| 376 |
+
});
|
| 377 |
+
if (!fixtureResponse.ok) {
|
| 378 |
+
throw new Error(
|
| 379 |
+
`CPU reference fixture request failed with HTTP ${fixtureResponse.status}.`,
|
| 380 |
+
);
|
| 381 |
+
}
|
| 382 |
+
const fixture = validateStateDriftReferenceFixture(
|
| 383 |
+
await fixtureResponse.json() as unknown,
|
| 384 |
+
{ model, capabilities, loadResult: warmLoad },
|
| 385 |
+
);
|
| 386 |
+
throwIfAborted(controller.signal);
|
| 387 |
+
setProgress({
|
| 388 |
+
label: 'Teacher-forcing the 1,024-token CPU reference',
|
| 389 |
+
loadedBytes: 0,
|
| 390 |
+
totalBytes: fixture.referenceTokenIds.length,
|
| 391 |
+
});
|
| 392 |
+
const score = await client.scoreSequence({
|
| 393 |
+
promptTokenIds: fixture.promptTokenIds,
|
| 394 |
+
referenceTokenIds: fixture.referenceTokenIds,
|
| 395 |
+
topK: 5,
|
| 396 |
+
}, { signal: controller.signal });
|
| 397 |
+
teacherForcedReferenceScore = { fixture, score };
|
| 398 |
+
throwIfAborted(controller.signal);
|
| 399 |
+
}
|
| 400 |
const backendReport = await client.backendReport();
|
| 401 |
throwIfAborted(controller.signal);
|
| 402 |
|
|
|
|
| 408 |
capabilities,
|
| 409 |
requestedBackend: backend,
|
| 410 |
contextSize: warmLoad.context.size,
|
| 411 |
+
workloadId,
|
| 412 |
+
generationRequest,
|
| 413 |
coldLoadMs,
|
| 414 |
warmLoadMs,
|
| 415 |
coldCachedBytes,
|
|
|
|
| 420 |
timeToFirstTokenMs: firstTokenAt === null ? null : firstTokenAt - generationStarted,
|
| 421 |
streamedTokenEvents,
|
| 422 |
backendReport,
|
| 423 |
+
teacherForcedReferenceScore,
|
| 424 |
});
|
| 425 |
setReport(nextReport);
|
| 426 |
+
setOutput(generationResult.text || generationResult.reasoningText);
|
| 427 |
setProgress({
|
| 428 |
label: 'Benchmark complete · JSON report is ready',
|
| 429 |
loadedBytes: 1,
|
|
|
|
| 473 |
<p className="bench-control-intro">The fixed prompt and temperature stay locked so exported runs remain comparable.</p>
|
| 474 |
|
| 475 |
<form onSubmit={(event) => { event.preventDefault(); runBenchmark(); }}>
|
| 476 |
+
<label className="bench-field">
|
| 477 |
+
<span>Workload</span>
|
| 478 |
+
<select
|
| 479 |
+
value={workloadId}
|
| 480 |
+
disabled={running}
|
| 481 |
+
onChange={(event) => {
|
| 482 |
+
const nextId = event.target.value as BenchmarkWorkloadId;
|
| 483 |
+
const next = BENCHMARK_WORKLOADS[nextId];
|
| 484 |
+
setWorkloadId(nextId);
|
| 485 |
+
setMaxTokens(next.requiredMaxTokens ?? 64);
|
| 486 |
+
clearPreviousRun();
|
| 487 |
+
}}
|
| 488 |
+
>
|
| 489 |
+
{Object.values(BENCHMARK_WORKLOADS).map((candidate) => (
|
| 490 |
+
<option key={candidate.id} value={candidate.id}>{candidate.label}</option>
|
| 491 |
+
))}
|
| 492 |
+
</select>
|
| 493 |
+
<small>{workload.description}</small>
|
| 494 |
+
</label>
|
| 495 |
+
|
| 496 |
<label className="bench-field">
|
| 497 |
<span>Model tier</span>
|
| 498 |
<select
|
|
|
|
| 546 |
<input
|
| 547 |
type="number"
|
| 548 |
min="8"
|
| 549 |
+
max="1024"
|
| 550 |
step="8"
|
| 551 |
value={maxTokens}
|
| 552 |
disabled={running}
|
|
|
|
| 555 |
clearPreviousRun();
|
| 556 |
}}
|
| 557 |
/>
|
| 558 |
+
<small>8–1,024</small>
|
| 559 |
</label>
|
| 560 |
</div>
|
| 561 |
|
|
|
|
| 737 |
<div><dt>Decode</dt><dd>{formatRate(report?.generation.decodeTokensPerSecond)}</dd></div>
|
| 738 |
<div><dt>Token count</dt><dd>{report?.generation.completionTokens ?? '—'}</dd></div>
|
| 739 |
<div><dt>Finish</dt><dd>{report?.generation.finishReason ?? '—'}</dd></div>
|
| 740 |
+
<div><dt>CPU-ref PPL</dt><dd>{report?.generation.teacherForcedReferenceScore?.score.summary.perplexity.toFixed(4) ?? '—'}</dd></div>
|
| 741 |
+
<div><dt>CPU-ref mean NLL</dt><dd>{report?.generation.teacherForcedReferenceScore?.score.summary.meanNll.toFixed(6) ?? '—'}</dd></div>
|
| 742 |
</dl>
|
| 743 |
</article>
|
| 744 |
<article className="bench-data-card">
|
|
|
|
| 774 |
<section className="bench-output">
|
| 775 |
<div>
|
| 776 |
<p className="eyebrow ink">Fixed specimen</p>
|
| 777 |
+
<h3>{workload.id}</h3>
|
| 778 |
+
<p>{workload.messages.map((message) => message.content ?? '').join('\n')}</p>
|
| 779 |
</div>
|
| 780 |
<div>
|
| 781 |
+
<p className="eyebrow ink">Generated output · visible text, else reasoning trace</p>
|
| 782 |
<p data-testid="bench-generated-output">{output || 'Run the assay to inspect the generated specimen.'}</p>
|
| 783 |
</div>
|
| 784 |
</section>
|
src/bench/report.test.ts
CHANGED
|
@@ -2,16 +2,21 @@ import { describe, expect, it } from 'vitest';
|
|
| 2 |
import type {
|
| 3 |
BackendReport,
|
| 4 |
EngineCapabilities,
|
|
|
|
| 5 |
LoadModelResult,
|
| 6 |
ManifestModelV2,
|
| 7 |
ModelManifestV2,
|
| 8 |
} from '../engine';
|
| 9 |
import {
|
|
|
|
|
|
|
|
|
|
| 10 |
benchmarkReportFilename,
|
| 11 |
buildBenchmarkReport,
|
| 12 |
classifyCacheState,
|
| 13 |
serializeBenchmarkReport,
|
| 14 |
type BenchmarkObservation,
|
|
|
|
| 15 |
} from './report';
|
| 16 |
|
| 17 |
const JSPI_WASM_SHA256 = 'd'.repeat(64);
|
|
@@ -33,6 +38,18 @@ const model = {
|
|
| 33 |
id: '8b',
|
| 34 |
displayName: 'Bonsai 8B',
|
| 35 |
architecture: 'qwen3next',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
downloadBytes: 1_000,
|
| 37 |
} as ManifestModelV2;
|
| 38 |
|
|
@@ -150,6 +167,96 @@ const loadResult = {
|
|
| 150 |
backendReport,
|
| 151 |
} satisfies LoadModelResult;
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkObservation {
|
| 154 |
return {
|
| 155 |
startedAt: '2026-07-15T10:00:00.000Z',
|
|
@@ -159,7 +266,11 @@ function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkOb
|
|
| 159 |
capabilities,
|
| 160 |
requestedBackend: 'auto',
|
| 161 |
contextSize: 4_096,
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
coldLoadMs: 1_234.56,
|
| 164 |
warmLoadMs: 234.54,
|
| 165 |
coldCachedBytes: 250,
|
|
@@ -167,6 +278,9 @@ function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkOb
|
|
| 167 |
loadResult,
|
| 168 |
generationResult: {
|
| 169 |
text: 'fixture',
|
|
|
|
|
|
|
|
|
|
| 170 |
finishReason: 'stop',
|
| 171 |
toolCalls: [],
|
| 172 |
usage: { promptTokens: 20, completionTokens: 8, totalTokens: 28 },
|
|
@@ -180,6 +294,87 @@ function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkOb
|
|
| 180 |
};
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
describe('benchmark report', () => {
|
| 184 |
it('classifies the shard cache observed at the first load boundary', () => {
|
| 185 |
expect(classifyCacheState(null, 1_000)).toBe('unknown');
|
|
@@ -191,6 +386,11 @@ describe('benchmark report', () => {
|
|
| 191 |
it('builds a shareable report with pinned runtime evidence and fixed-safe policy', () => {
|
| 192 |
const report = buildBenchmarkReport(observation());
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
expect(report.load).toMatchObject({
|
| 195 |
tuning: {
|
| 196 |
scope: 'release-defaults',
|
|
@@ -219,12 +419,38 @@ describe('benchmark report', () => {
|
|
| 219 |
warm: { durationMs: 234.5 },
|
| 220 |
});
|
| 221 |
expect(report.generation).toMatchObject({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
promptTokensPerSecond: 123.46,
|
| 223 |
decodeTokensPerSecond: 45.68,
|
| 224 |
engineCompletionTokens: 8,
|
| 225 |
streamedTokenEvents: 5,
|
| 226 |
completionTokens: 8,
|
| 227 |
-
tokenCountSource: '
|
| 228 |
});
|
| 229 |
expect(report.execution).toMatchObject({
|
| 230 |
selectedBackend: 'webgpu',
|
|
@@ -345,6 +571,17 @@ describe('benchmark report', () => {
|
|
| 345 |
});
|
| 346 |
});
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
it('exports the selected model DFlash pairing for unsupported 1.7B and conditional 27B', () => {
|
| 349 |
const reportFor = (modelId: ManifestModelV2['id']) => buildBenchmarkReport(observation({
|
| 350 |
model: {
|
|
@@ -378,11 +615,14 @@ describe('benchmark report', () => {
|
|
| 378 |
expect(exportedPairing.targetLayerIdsZeroBased).not.toBe(dflash27bPairing.targetLayerIdsZeroBased);
|
| 379 |
});
|
| 380 |
|
| 381 |
-
it('uses
|
| 382 |
const report = buildBenchmarkReport(observation({
|
| 383 |
streamedTokenEvents: 64,
|
| 384 |
generationResult: {
|
| 385 |
text: 'fixture',
|
|
|
|
|
|
|
|
|
|
| 386 |
finishReason: 'length',
|
| 387 |
toolCalls: [],
|
| 388 |
usage: { promptTokens: 5, completionTokens: 0, totalTokens: 5 },
|
|
@@ -396,16 +636,22 @@ describe('benchmark report', () => {
|
|
| 396 |
completionTokens: 64,
|
| 397 |
engineTotalTokens: 5,
|
| 398 |
totalTokens: 69,
|
| 399 |
-
tokenCountSource: '
|
| 400 |
});
|
|
|
|
|
|
|
|
|
|
| 401 |
});
|
| 402 |
|
| 403 |
-
it('
|
| 404 |
const report = buildBenchmarkReport(observation({
|
| 405 |
generationElapsedMs: Number.NaN,
|
| 406 |
timeToFirstTokenMs: Number.POSITIVE_INFINITY,
|
| 407 |
generationResult: {
|
| 408 |
text: 'fixture',
|
|
|
|
|
|
|
|
|
|
| 409 |
finishReason: 'length',
|
| 410 |
toolCalls: [],
|
| 411 |
usage: null,
|
|
@@ -418,12 +664,228 @@ describe('benchmark report', () => {
|
|
| 418 |
timeToFirstTokenMs: null,
|
| 419 |
decodeTokensPerSecond: null,
|
| 420 |
completionTokens: 5,
|
| 421 |
-
tokenCountSource: '
|
| 422 |
});
|
| 423 |
expect(serializeBenchmarkReport(report)).not.toContain('NaN');
|
| 424 |
expect(serializeBenchmarkReport(report)).toMatch(/\n$/);
|
| 425 |
});
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
it('uses the model, selected backend, and UTC completion time in the export name', () => {
|
| 428 |
const report = buildBenchmarkReport(observation());
|
| 429 |
expect(benchmarkReportFilename(report)).toBe(
|
|
|
|
| 2 |
import type {
|
| 3 |
BackendReport,
|
| 4 |
EngineCapabilities,
|
| 5 |
+
GenerateParams,
|
| 6 |
LoadModelResult,
|
| 7 |
ManifestModelV2,
|
| 8 |
ModelManifestV2,
|
| 9 |
} from '../engine';
|
| 10 |
import {
|
| 11 |
+
BENCHMARK_WORKLOADS,
|
| 12 |
+
DEFAULT_BENCHMARK_WORKLOAD_ID,
|
| 13 |
+
TOKEN_ID_CHECKPOINTS,
|
| 14 |
benchmarkReportFilename,
|
| 15 |
buildBenchmarkReport,
|
| 16 |
classifyCacheState,
|
| 17 |
serializeBenchmarkReport,
|
| 18 |
type BenchmarkObservation,
|
| 19 |
+
type TeacherForcedReferenceScoreEvidence,
|
| 20 |
} from './report';
|
| 21 |
|
| 22 |
const JSPI_WASM_SHA256 = 'd'.repeat(64);
|
|
|
|
| 38 |
id: '8b',
|
| 39 |
displayName: 'Bonsai 8B',
|
| 40 |
architecture: 'qwen3next',
|
| 41 |
+
source: {
|
| 42 |
+
repo: 'prism-ml/Bonsai-8B-gguf',
|
| 43 |
+
revision: '0123456789abcdef0123456789abcdef01234567',
|
| 44 |
+
file: 'Bonsai-8B-Q1_0.gguf',
|
| 45 |
+
bytes: 1_000,
|
| 46 |
+
sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
| 47 |
+
},
|
| 48 |
+
files: [{
|
| 49 |
+
path: '8b/Bonsai-8B-Q1_0.gguf',
|
| 50 |
+
bytes: 1_000,
|
| 51 |
+
sha256: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
| 52 |
+
}],
|
| 53 |
downloadBytes: 1_000,
|
| 54 |
} as ManifestModelV2;
|
| 55 |
|
|
|
|
| 167 |
backendReport,
|
| 168 |
} satisfies LoadModelResult;
|
| 169 |
|
| 170 |
+
const fieldCoreGenerationRequest: GenerateParams = {
|
| 171 |
+
messages: BENCHMARK_WORKLOADS[DEFAULT_BENCHMARK_WORKLOAD_ID].messages.map((message) => ({
|
| 172 |
+
...message,
|
| 173 |
+
})),
|
| 174 |
+
maxTokens: 64,
|
| 175 |
+
temperature: 0,
|
| 176 |
+
topP: 1,
|
| 177 |
+
topK: 1,
|
| 178 |
+
seed: 42,
|
| 179 |
+
toolChoice: 'none',
|
| 180 |
+
cachePrompt: false,
|
| 181 |
+
returnTokenIds: true,
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
function sampledTokenTrace(tokenIds: readonly number[]) {
|
| 185 |
+
return tokenIds.map((id) => ({
|
| 186 |
+
selected: { id, logprob: -0.1 },
|
| 187 |
+
topCandidates: [
|
| 188 |
+
{ id, logprob: -0.1 },
|
| 189 |
+
{ id: id + 10_001, logprob: -1 },
|
| 190 |
+
{ id: id + 10_002, logprob: -2 },
|
| 191 |
+
{ id: id + 10_003, logprob: -3 },
|
| 192 |
+
{ id: id + 10_004, logprob: -4 },
|
| 193 |
+
],
|
| 194 |
+
}));
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
function teacherForcedReferenceScore(
|
| 198 |
+
referenceTokenIds: readonly number[],
|
| 199 |
+
): TeacherForcedReferenceScoreEvidence {
|
| 200 |
+
const entries = referenceTokenIds.map((id, index) => ({
|
| 201 |
+
index,
|
| 202 |
+
selectedReference: { id, logprob: -0.1 },
|
| 203 |
+
naturalTop1: { id, logprob: -0.1 },
|
| 204 |
+
topCandidates: [
|
| 205 |
+
{ id, logprob: -0.1 },
|
| 206 |
+
{ id: id + 10_001, logprob: -1 },
|
| 207 |
+
{ id: id + 10_002, logprob: -2 },
|
| 208 |
+
{ id: id + 10_003, logprob: -3 },
|
| 209 |
+
{ id: id + 10_004, logprob: -4 },
|
| 210 |
+
],
|
| 211 |
+
referenceRankInTopCandidatesZeroBased: 0,
|
| 212 |
+
top1Top2Margin: 0.9,
|
| 213 |
+
}));
|
| 214 |
+
return {
|
| 215 |
+
fixture: {
|
| 216 |
+
schemaVersion: 1,
|
| 217 |
+
kind: 'bonsai-state-drift-reference',
|
| 218 |
+
workload: {
|
| 219 |
+
id: 'state-drift-1k-v1',
|
| 220 |
+
messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({
|
| 221 |
+
role: 'user',
|
| 222 |
+
content: message.content ?? '',
|
| 223 |
+
})),
|
| 224 |
+
},
|
| 225 |
+
provenance: {
|
| 226 |
+
sourceEvidence: 'results/space-model/fixture.json',
|
| 227 |
+
engineRevision: capabilities.runtime.llamaCppRevision,
|
| 228 |
+
nativeBinary: { bytes: 1, sha256: '1'.repeat(64) },
|
| 229 |
+
model: { file: model.source.file, bytes: model.source.bytes, sha256: model.source.sha256 },
|
| 230 |
+
renderedPromptSha256: '2'.repeat(64),
|
| 231 |
+
execution: { backend: 'cpu', contextSize: 4_096, batchSize: 2_048, microBatchSize: 512 },
|
| 232 |
+
},
|
| 233 |
+
tokenEncoding: 'uint32-le',
|
| 234 |
+
promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
|
| 235 |
+
promptTokenIdsSha256: '3'.repeat(64),
|
| 236 |
+
referenceTokenIds: [...referenceTokenIds],
|
| 237 |
+
referenceTokenIdsSha256: '4'.repeat(64),
|
| 238 |
+
checkpointPrefixes: TOKEN_ID_CHECKPOINTS.map((tokens) => ({
|
| 239 |
+
tokens,
|
| 240 |
+
sha256: '5'.repeat(64),
|
| 241 |
+
})),
|
| 242 |
+
},
|
| 243 |
+
score: {
|
| 244 |
+
method: {
|
| 245 |
+
promptMode: 'raw-token-id-prefix',
|
| 246 |
+
maxTokensPerStep: 1,
|
| 247 |
+
temperature: 0,
|
| 248 |
+
topK: 1,
|
| 249 |
+
reportedTopLogprobs: 5,
|
| 250 |
+
logitBias: 1_000,
|
| 251 |
+
cachePromptFirst: false,
|
| 252 |
+
cachePromptSubsequent: true,
|
| 253 |
+
},
|
| 254 |
+
entries,
|
| 255 |
+
summary: { tokenCount: 1_024, meanNll: 0.1, perplexity: Math.exp(0.1) },
|
| 256 |
+
},
|
| 257 |
+
};
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkObservation {
|
| 261 |
return {
|
| 262 |
startedAt: '2026-07-15T10:00:00.000Z',
|
|
|
|
| 266 |
capabilities,
|
| 267 |
requestedBackend: 'auto',
|
| 268 |
contextSize: 4_096,
|
| 269 |
+
workloadId: DEFAULT_BENCHMARK_WORKLOAD_ID,
|
| 270 |
+
generationRequest: {
|
| 271 |
+
...fieldCoreGenerationRequest,
|
| 272 |
+
messages: fieldCoreGenerationRequest.messages.map((message) => ({ ...message })),
|
| 273 |
+
},
|
| 274 |
coldLoadMs: 1_234.56,
|
| 275 |
warmLoadMs: 234.54,
|
| 276 |
coldCachedBytes: 250,
|
|
|
|
| 278 |
loadResult,
|
| 279 |
generationResult: {
|
| 280 |
text: 'fixture',
|
| 281 |
+
reasoningText: '',
|
| 282 |
+
tokenIds: [101, 102, 103, 104, 105, 106, 107, 108],
|
| 283 |
+
tokenTrace: sampledTokenTrace([101, 102, 103, 104, 105, 106, 107, 108]),
|
| 284 |
finishReason: 'stop',
|
| 285 |
toolCalls: [],
|
| 286 |
usage: { promptTokens: 20, completionTokens: 8, totalTokens: 28 },
|
|
|
|
| 294 |
};
|
| 295 |
}
|
| 296 |
|
| 297 |
+
function longContextObservation(): BenchmarkObservation {
|
| 298 |
+
const longContextBackendReport = {
|
| 299 |
+
...backendReport,
|
| 300 |
+
layersGpu: { offloaded: 65, total: 65 },
|
| 301 |
+
flashAttention: true,
|
| 302 |
+
cacheTypeK: 'q4_0',
|
| 303 |
+
cacheTypeV: 'q4_0',
|
| 304 |
+
webgpuKvBufferBytes: 155_713_536,
|
| 305 |
+
} satisfies BackendReport;
|
| 306 |
+
const longContextLoadResult = {
|
| 307 |
+
...loadResult,
|
| 308 |
+
modelId: '27b',
|
| 309 |
+
gate: {
|
| 310 |
+
...loadResult.gate,
|
| 311 |
+
requestedBackend: 'webgpu',
|
| 312 |
+
selectedBackend: 'webgpu',
|
| 313 |
+
},
|
| 314 |
+
context: { ...loadResult.context, size: 8_448, layerCount: 65 },
|
| 315 |
+
tuning: {
|
| 316 |
+
scope: 'benchmark',
|
| 317 |
+
requested: {
|
| 318 |
+
nBatch: 32,
|
| 319 |
+
nUbatch: 16,
|
| 320 |
+
flashMode: 'auto',
|
| 321 |
+
cacheTypeK: 'q4_0',
|
| 322 |
+
cacheTypeV: 'q4_0',
|
| 323 |
+
wasmFlavor: 'auto',
|
| 324 |
+
},
|
| 325 |
+
observed: {
|
| 326 |
+
nBatch: 32,
|
| 327 |
+
nUbatch: 16,
|
| 328 |
+
flashAttention: true,
|
| 329 |
+
cacheTypeK: 'q4_0',
|
| 330 |
+
cacheTypeV: 'q4_0',
|
| 331 |
+
kvBufferBytes: 155_713_536,
|
| 332 |
+
wasmFlavor: 'jspi',
|
| 333 |
+
wasmSha256: JSPI_WASM_SHA256,
|
| 334 |
+
compatWorkerSha256: null,
|
| 335 |
+
},
|
| 336 |
+
applied: true,
|
| 337 |
+
},
|
| 338 |
+
backendReport: longContextBackendReport,
|
| 339 |
+
} satisfies LoadModelResult;
|
| 340 |
+
|
| 341 |
+
return observation({
|
| 342 |
+
model: {
|
| 343 |
+
...model,
|
| 344 |
+
id: '27b',
|
| 345 |
+
displayName: 'Bonsai 27B',
|
| 346 |
+
architecture: 'qwen35',
|
| 347 |
+
} as ManifestModelV2,
|
| 348 |
+
requestedBackend: 'webgpu',
|
| 349 |
+
contextSize: 8_448,
|
| 350 |
+
workloadId: 'context-prefill-8k-v1',
|
| 351 |
+
generationRequest: {
|
| 352 |
+
messages: BENCHMARK_WORKLOADS['context-prefill-8k-v1'].messages.map((message) => ({ ...message })),
|
| 353 |
+
maxTokens: 8,
|
| 354 |
+
temperature: 0,
|
| 355 |
+
topP: 1,
|
| 356 |
+
topK: 1,
|
| 357 |
+
seed: 42,
|
| 358 |
+
toolChoice: 'none',
|
| 359 |
+
cachePrompt: false,
|
| 360 |
+
returnTokenIds: true,
|
| 361 |
+
},
|
| 362 |
+
loadResult: longContextLoadResult,
|
| 363 |
+
backendReport: longContextBackendReport,
|
| 364 |
+
streamedTokenEvents: 8,
|
| 365 |
+
generationResult: {
|
| 366 |
+
text: 'fixture',
|
| 367 |
+
reasoningText: '',
|
| 368 |
+
tokenIds: [201, 202, 203, 204, 205, 206, 207, 208],
|
| 369 |
+
tokenTrace: sampledTokenTrace([201, 202, 203, 204, 205, 206, 207, 208]),
|
| 370 |
+
finishReason: 'length',
|
| 371 |
+
toolCalls: [],
|
| 372 |
+
usage: { promptTokens: 8_314, completionTokens: 8, totalTokens: 8_322 },
|
| 373 |
+
timings: { promptTokensPerSecond: 20, predictedTokensPerSecond: 30 },
|
| 374 |
+
},
|
| 375 |
+
});
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
describe('benchmark report', () => {
|
| 379 |
it('classifies the shard cache observed at the first load boundary', () => {
|
| 380 |
expect(classifyCacheState(null, 1_000)).toBe('unknown');
|
|
|
|
| 386 |
it('builds a shareable report with pinned runtime evidence and fixed-safe policy', () => {
|
| 387 |
const report = buildBenchmarkReport(observation());
|
| 388 |
|
| 389 |
+
expect(report.schemaVersion).toBe(3);
|
| 390 |
+
expect(report.model).toMatchObject({
|
| 391 |
+
source: model.source,
|
| 392 |
+
shards: model.files,
|
| 393 |
+
});
|
| 394 |
expect(report.load).toMatchObject({
|
| 395 |
tuning: {
|
| 396 |
scope: 'release-defaults',
|
|
|
|
| 419 |
warm: { durationMs: 234.5 },
|
| 420 |
});
|
| 421 |
expect(report.generation).toMatchObject({
|
| 422 |
+
workload: {
|
| 423 |
+
id: 'field-core-v1',
|
| 424 |
+
messages: BENCHMARK_WORKLOADS['field-core-v1'].messages,
|
| 425 |
+
},
|
| 426 |
+
sampling: {
|
| 427 |
+
maxTokens: 64,
|
| 428 |
+
temperature: 0,
|
| 429 |
+
topP: 1,
|
| 430 |
+
topK: 1,
|
| 431 |
+
minP: null,
|
| 432 |
+
seed: 42,
|
| 433 |
+
toolChoice: 'none',
|
| 434 |
+
cachePrompt: false,
|
| 435 |
+
},
|
| 436 |
+
tokenTrace: {
|
| 437 |
+
returnTokenIds: true,
|
| 438 |
+
logprobs: true,
|
| 439 |
+
topLogprobs: 5,
|
| 440 |
+
topAlternatives: 4,
|
| 441 |
+
encoding: 'uint32-le',
|
| 442 |
+
tokenIds: [101, 102, 103, 104, 105, 106, 107, 108],
|
| 443 |
+
entries: sampledTokenTrace([101, 102, 103, 104, 105, 106, 107, 108]),
|
| 444 |
+
checkpointPrefixes: [],
|
| 445 |
+
},
|
| 446 |
+
rawText: 'fixture',
|
| 447 |
+
rawReasoningText: '',
|
| 448 |
promptTokensPerSecond: 123.46,
|
| 449 |
decodeTokensPerSecond: 45.68,
|
| 450 |
engineCompletionTokens: 8,
|
| 451 |
streamedTokenEvents: 5,
|
| 452 |
completionTokens: 8,
|
| 453 |
+
tokenCountSource: 'token-id-trace',
|
| 454 |
});
|
| 455 |
expect(report.execution).toMatchObject({
|
| 456 |
selectedBackend: 'webgpu',
|
|
|
|
| 571 |
});
|
| 572 |
});
|
| 573 |
|
| 574 |
+
it('exports reasoning-only output separately from visible content', () => {
|
| 575 |
+
const input = observation();
|
| 576 |
+
input.generationResult.text = '';
|
| 577 |
+
input.generationResult.reasoningText = 'reasoning-only fixture';
|
| 578 |
+
|
| 579 |
+
const report = buildBenchmarkReport(input);
|
| 580 |
+
|
| 581 |
+
expect(report.generation.rawText).toBe('');
|
| 582 |
+
expect(report.generation.rawReasoningText).toBe('reasoning-only fixture');
|
| 583 |
+
});
|
| 584 |
+
|
| 585 |
it('exports the selected model DFlash pairing for unsupported 1.7B and conditional 27B', () => {
|
| 586 |
const reportFor = (modelId: ManifestModelV2['id']) => buildBenchmarkReport(observation({
|
| 587 |
model: {
|
|
|
|
| 615 |
expect(exportedPairing.targetLayerIdsZeroBased).not.toBe(dflash27bPairing.targetLayerIdsZeroBased);
|
| 616 |
});
|
| 617 |
|
| 618 |
+
it('uses the exact token-id trace when final engine usage is present but incomplete', () => {
|
| 619 |
const report = buildBenchmarkReport(observation({
|
| 620 |
streamedTokenEvents: 64,
|
| 621 |
generationResult: {
|
| 622 |
text: 'fixture',
|
| 623 |
+
reasoningText: '',
|
| 624 |
+
tokenIds: Array.from({ length: 64 }, (_, index) => index + 1),
|
| 625 |
+
tokenTrace: sampledTokenTrace(Array.from({ length: 64 }, (_, index) => index + 1)),
|
| 626 |
finishReason: 'length',
|
| 627 |
toolCalls: [],
|
| 628 |
usage: { promptTokens: 5, completionTokens: 0, totalTokens: 5 },
|
|
|
|
| 636 |
completionTokens: 64,
|
| 637 |
engineTotalTokens: 5,
|
| 638 |
totalTokens: 69,
|
| 639 |
+
tokenCountSource: 'token-id-trace',
|
| 640 |
});
|
| 641 |
+
expect(report.generation.tokenTrace.checkpointPrefixes).toEqual([
|
| 642 |
+
{ tokens: 64, sha256: '0c8f462927e331f28e3f1a6d342957cd27118febc309bd3b2f646e2dfbaeec32' },
|
| 643 |
+
]);
|
| 644 |
});
|
| 645 |
|
| 646 |
+
it('uses the trace without emitting non-finite JSON metrics', () => {
|
| 647 |
const report = buildBenchmarkReport(observation({
|
| 648 |
generationElapsedMs: Number.NaN,
|
| 649 |
timeToFirstTokenMs: Number.POSITIVE_INFINITY,
|
| 650 |
generationResult: {
|
| 651 |
text: 'fixture',
|
| 652 |
+
reasoningText: '',
|
| 653 |
+
tokenIds: [1, 2, 3, 4, 5],
|
| 654 |
+
tokenTrace: sampledTokenTrace([1, 2, 3, 4, 5]),
|
| 655 |
finishReason: 'length',
|
| 656 |
toolCalls: [],
|
| 657 |
usage: null,
|
|
|
|
| 664 |
timeToFirstTokenMs: null,
|
| 665 |
decodeTokensPerSecond: null,
|
| 666 |
completionTokens: 5,
|
| 667 |
+
tokenCountSource: 'token-id-trace',
|
| 668 |
});
|
| 669 |
expect(serializeBenchmarkReport(report)).not.toContain('NaN');
|
| 670 |
expect(serializeBenchmarkReport(report)).toMatch(/\n$/);
|
| 671 |
});
|
| 672 |
|
| 673 |
+
it('records all locked state-drift checkpoints from the exact token-id prefixes', () => {
|
| 674 |
+
const tokenIds = Array.from({ length: 1_024 }, (_, index) => index + 1);
|
| 675 |
+
const report = buildBenchmarkReport(observation({
|
| 676 |
+
workloadId: 'state-drift-1k-v1',
|
| 677 |
+
generationRequest: {
|
| 678 |
+
messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({ ...message })),
|
| 679 |
+
maxTokens: 1_024,
|
| 680 |
+
temperature: 0,
|
| 681 |
+
topP: 1,
|
| 682 |
+
topK: 1,
|
| 683 |
+
seed: 42,
|
| 684 |
+
toolChoice: 'none',
|
| 685 |
+
cachePrompt: false,
|
| 686 |
+
returnTokenIds: true,
|
| 687 |
+
},
|
| 688 |
+
streamedTokenEvents: 1_024,
|
| 689 |
+
generationResult: {
|
| 690 |
+
text: '1, 2, 3, …',
|
| 691 |
+
reasoningText: '',
|
| 692 |
+
tokenIds,
|
| 693 |
+
tokenTrace: sampledTokenTrace(tokenIds),
|
| 694 |
+
finishReason: 'length',
|
| 695 |
+
toolCalls: [],
|
| 696 |
+
usage: { promptTokens: 20, completionTokens: 1_024, totalTokens: 1_044 },
|
| 697 |
+
timings: { promptTokensPerSecond: 20, predictedTokensPerSecond: 30 },
|
| 698 |
+
},
|
| 699 |
+
teacherForcedReferenceScore: teacherForcedReferenceScore(tokenIds),
|
| 700 |
+
}));
|
| 701 |
+
|
| 702 |
+
expect(report.generation.workload).toEqual({
|
| 703 |
+
id: 'state-drift-1k-v1',
|
| 704 |
+
messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages,
|
| 705 |
+
});
|
| 706 |
+
expect(report.generation.rawText).toBe('1, 2, 3, …');
|
| 707 |
+
expect(report.generation.rawReasoningText).toBe('');
|
| 708 |
+
expect(report.generation.tokenTrace.tokenIds).toEqual(tokenIds);
|
| 709 |
+
expect(report.generation.tokenTrace.entries).toHaveLength(1_024);
|
| 710 |
+
expect(report.generation.teacherForcedReferenceScore).toMatchObject({
|
| 711 |
+
fixture: { kind: 'bonsai-state-drift-reference' },
|
| 712 |
+
score: { summary: { tokenCount: 1_024, meanNll: 0.1 } },
|
| 713 |
+
});
|
| 714 |
+
expect(report.generation.tokenTrace.checkpointPrefixes).toEqual([
|
| 715 |
+
{ tokens: 64, sha256: '0c8f462927e331f28e3f1a6d342957cd27118febc309bd3b2f646e2dfbaeec32' },
|
| 716 |
+
{ tokens: 128, sha256: '24f9ac547baae524ba0ea5220692d48f7526cdb1df5e99edcbb1f32239a8d5f5' },
|
| 717 |
+
{ tokens: 256, sha256: '80fa0f6d1caca9aad2b012051399b33bcd1976b145f3f3eea0f7ba10637761b0' },
|
| 718 |
+
{ tokens: 512, sha256: '8ab9b2cf36ec9e9d68711df73334731d2fee0552f5d95d76538b1d2cdcefd564' },
|
| 719 |
+
{ tokens: 768, sha256: '23905fc64dc47867f49672959cddf676ca257967f5786eefaf8583ddf7ddf3e9' },
|
| 720 |
+
{ tokens: 1_024, sha256: '6b8b6bd30ff821daf5db90cc071525f5696e366efd1aeb30d0bf60f785a3c26d' },
|
| 721 |
+
]);
|
| 722 |
+
});
|
| 723 |
+
|
| 724 |
+
it('exports null teacher-forced scoring for non-state-drift workloads', () => {
|
| 725 |
+
expect(buildBenchmarkReport(observation()).generation.teacherForcedReferenceScore).toBeNull();
|
| 726 |
+
});
|
| 727 |
+
|
| 728 |
+
it('rejects incomplete state-drift traces instead of exporting partial evidence', () => {
|
| 729 |
+
expect(() => buildBenchmarkReport(observation({
|
| 730 |
+
workloadId: 'state-drift-1k-v1',
|
| 731 |
+
generationRequest: {
|
| 732 |
+
messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({ ...message })),
|
| 733 |
+
maxTokens: 1_024,
|
| 734 |
+
temperature: 0,
|
| 735 |
+
topP: 1,
|
| 736 |
+
topK: 1,
|
| 737 |
+
seed: 42,
|
| 738 |
+
toolChoice: 'none',
|
| 739 |
+
cachePrompt: false,
|
| 740 |
+
returnTokenIds: true,
|
| 741 |
+
},
|
| 742 |
+
generationResult: {
|
| 743 |
+
text: 'partial fixture',
|
| 744 |
+
reasoningText: '',
|
| 745 |
+
tokenIds: Array.from({ length: 1_023 }, (_, index) => index + 1),
|
| 746 |
+
tokenTrace: sampledTokenTrace(Array.from({ length: 1_023 }, (_, index) => index + 1)),
|
| 747 |
+
finishReason: 'length',
|
| 748 |
+
toolCalls: [],
|
| 749 |
+
usage: { promptTokens: 20, completionTokens: 1_023, totalTokens: 1_043 },
|
| 750 |
+
timings: null,
|
| 751 |
+
},
|
| 752 |
+
}))).toThrow('requires exactly 1,024 sampled token ids');
|
| 753 |
+
});
|
| 754 |
+
|
| 755 |
+
it('rejects state-drift traces that stop before exhausting the locked budget', () => {
|
| 756 |
+
const input = observation({
|
| 757 |
+
workloadId: 'state-drift-1k-v1',
|
| 758 |
+
generationRequest: {
|
| 759 |
+
messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({ ...message })),
|
| 760 |
+
maxTokens: 1_024,
|
| 761 |
+
temperature: 0,
|
| 762 |
+
topP: 1,
|
| 763 |
+
topK: 1,
|
| 764 |
+
seed: 42,
|
| 765 |
+
toolChoice: 'none',
|
| 766 |
+
cachePrompt: false,
|
| 767 |
+
returnTokenIds: true,
|
| 768 |
+
},
|
| 769 |
+
generationResult: {
|
| 770 |
+
text: 'early stop',
|
| 771 |
+
reasoningText: '',
|
| 772 |
+
tokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1),
|
| 773 |
+
tokenTrace: sampledTokenTrace(Array.from({ length: 1_024 }, (_, index) => index + 1)),
|
| 774 |
+
finishReason: 'stop',
|
| 775 |
+
toolCalls: [],
|
| 776 |
+
usage: { promptTokens: 20, completionTokens: 1_024, totalTokens: 1_044 },
|
| 777 |
+
timings: null,
|
| 778 |
+
},
|
| 779 |
+
});
|
| 780 |
+
|
| 781 |
+
expect(() => buildBenchmarkReport(input)).toThrow('requires finishReason=length');
|
| 782 |
+
});
|
| 783 |
+
|
| 784 |
+
it('exports long-context evidence only after processing more than 8K prompt tokens', () => {
|
| 785 |
+
const report = buildBenchmarkReport(longContextObservation());
|
| 786 |
+
|
| 787 |
+
expect(BENCHMARK_WORKLOADS['context-prefill-8k-v1'].messages).toEqual([{
|
| 788 |
+
role: 'user',
|
| 789 |
+
content: `Continue the pattern.${' x'.repeat(8_300)}`,
|
| 790 |
+
}]);
|
| 791 |
+
expect(report).toMatchObject({
|
| 792 |
+
model: { id: '27b', contextSize: 8_448 },
|
| 793 |
+
generation: {
|
| 794 |
+
workload: { id: 'context-prefill-8k-v1' },
|
| 795 |
+
sampling: { maxTokens: 8 },
|
| 796 |
+
promptTokens: 8_314,
|
| 797 |
+
completionTokens: 8,
|
| 798 |
+
},
|
| 799 |
+
load: {
|
| 800 |
+
tuning: {
|
| 801 |
+
scope: 'benchmark',
|
| 802 |
+
requested: { flashMode: 'auto', cacheTypeK: 'q4_0', cacheTypeV: 'q4_0' },
|
| 803 |
+
observed: { flashAttention: true, cacheTypeK: 'q4_0', cacheTypeV: 'q4_0' },
|
| 804 |
+
applied: true,
|
| 805 |
+
},
|
| 806 |
+
},
|
| 807 |
+
execution: { requestedBackend: 'webgpu', selectedBackend: 'webgpu' },
|
| 808 |
+
});
|
| 809 |
+
expect(report.generation.tokenTrace.tokenIds).toHaveLength(8);
|
| 810 |
+
});
|
| 811 |
+
|
| 812 |
+
it('rejects long-context evidence at or below 8,192 engine-counted prompt tokens', () => {
|
| 813 |
+
const input = longContextObservation();
|
| 814 |
+
input.generationResult.usage = { promptTokens: 8_192, completionTokens: 8, totalTokens: 8_200 };
|
| 815 |
+
|
| 816 |
+
expect(() => buildBenchmarkReport(input)).toThrow(
|
| 817 |
+
'requires more than 8,192 engine-counted prompt tokens',
|
| 818 |
+
);
|
| 819 |
+
});
|
| 820 |
+
|
| 821 |
+
it('rejects a long-context trace that stops before exhausting the 8-token budget', () => {
|
| 822 |
+
const input = longContextObservation();
|
| 823 |
+
input.generationResult.finishReason = 'stop';
|
| 824 |
+
|
| 825 |
+
expect(() => buildBenchmarkReport(input)).toThrow('requires finishReason=length');
|
| 826 |
+
});
|
| 827 |
+
|
| 828 |
+
it('rejects long-context evidence with the wrong context or runtime tuning', () => {
|
| 829 |
+
const wrongContext = longContextObservation();
|
| 830 |
+
wrongContext.contextSize = 8_447;
|
| 831 |
+
wrongContext.loadResult = {
|
| 832 |
+
...wrongContext.loadResult,
|
| 833 |
+
context: { ...wrongContext.loadResult.context, size: 8_447 },
|
| 834 |
+
};
|
| 835 |
+
expect(() => buildBenchmarkReport(wrongContext)).toThrow(
|
| 836 |
+
'requires Bonsai 27B at context 8,448',
|
| 837 |
+
);
|
| 838 |
+
|
| 839 |
+
const wrongTuning = longContextObservation();
|
| 840 |
+
wrongTuning.loadResult = {
|
| 841 |
+
...wrongTuning.loadResult,
|
| 842 |
+
tuning: {
|
| 843 |
+
...wrongTuning.loadResult.tuning,
|
| 844 |
+
requested: {
|
| 845 |
+
...wrongTuning.loadResult.tuning.requested,
|
| 846 |
+
cacheTypeK: 'q8_0',
|
| 847 |
+
cacheTypeV: 'q8_0',
|
| 848 |
+
},
|
| 849 |
+
observed: {
|
| 850 |
+
...wrongTuning.loadResult.tuning.observed,
|
| 851 |
+
cacheTypeK: 'q8_0',
|
| 852 |
+
cacheTypeV: 'q8_0',
|
| 853 |
+
},
|
| 854 |
+
},
|
| 855 |
+
};
|
| 856 |
+
expect(() => buildBenchmarkReport(wrongTuning)).toThrow(
|
| 857 |
+
'requires Bonsai 27B at context 8,448',
|
| 858 |
+
);
|
| 859 |
+
});
|
| 860 |
+
|
| 861 |
+
it('rejects missing and malformed token-id traces', () => {
|
| 862 |
+
const missingTrace = observation();
|
| 863 |
+
missingTrace.generationResult.tokenIds = null;
|
| 864 |
+
expect(() => buildBenchmarkReport(missingTrace)).toThrow('without a token-id trace');
|
| 865 |
+
|
| 866 |
+
const malformedTrace = observation();
|
| 867 |
+
malformedTrace.generationResult.tokenIds = [1, 2.5];
|
| 868 |
+
expect(() => buildBenchmarkReport(malformedTrace)).toThrow('token id 1 is invalid');
|
| 869 |
+
});
|
| 870 |
+
|
| 871 |
+
it('rejects missing, mismatched, and unsorted sampled-token logprob traces', () => {
|
| 872 |
+
const missingTrace = observation();
|
| 873 |
+
missingTrace.generationResult.tokenTrace = null;
|
| 874 |
+
expect(() => buildBenchmarkReport(missingTrace)).toThrow('without a sampled-token logprob trace');
|
| 875 |
+
|
| 876 |
+
const mismatchedTrace = observation();
|
| 877 |
+
mismatchedTrace.generationResult.tokenTrace?.pop();
|
| 878 |
+
expect(() => buildBenchmarkReport(mismatchedTrace)).toThrow(
|
| 879 |
+
'sampled-token trace length 7 does not match token-id length 8',
|
| 880 |
+
);
|
| 881 |
+
|
| 882 |
+
const unsortedTrace = observation();
|
| 883 |
+
const candidates = unsortedTrace.generationResult.tokenTrace?.[0]?.topCandidates;
|
| 884 |
+
if (!candidates) throw new Error('Unsorted trace fixture requires top candidates.');
|
| 885 |
+
[candidates[0], candidates[1]] = [candidates[1]!, candidates[0]!];
|
| 886 |
+
expect(() => buildBenchmarkReport(unsortedTrace)).toThrow('candidates are not sorted');
|
| 887 |
+
});
|
| 888 |
+
|
| 889 |
it('uses the model, selected backend, and UTC completion time in the export name', () => {
|
| 890 |
const report = buildBenchmarkReport(observation());
|
| 891 |
expect(benchmarkReportFilename(report)).toBe(
|
src/bench/report.ts
CHANGED
|
@@ -1,15 +1,65 @@
|
|
|
|
|
|
|
|
| 1 |
import type {
|
| 2 |
BackendReport,
|
| 3 |
EngineCapabilities,
|
|
|
|
|
|
|
|
|
|
| 4 |
GenerateResult,
|
| 5 |
LoadModelResult,
|
| 6 |
ManifestModelV2,
|
| 7 |
ModelManifestV2,
|
| 8 |
RequestedBackend,
|
|
|
|
| 9 |
} from '../engine';
|
|
|
|
| 10 |
|
| 11 |
-
export const
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
export type CacheState = 'empty' | 'partial' | 'cached' | 'unknown';
|
| 15 |
|
|
@@ -21,7 +71,8 @@ export interface BenchmarkObservation {
|
|
| 21 |
capabilities: EngineCapabilities;
|
| 22 |
requestedBackend: RequestedBackend;
|
| 23 |
contextSize: number;
|
| 24 |
-
|
|
|
|
| 25 |
coldLoadMs: number;
|
| 26 |
warmLoadMs: number;
|
| 27 |
coldCachedBytes: number | null;
|
|
@@ -32,10 +83,16 @@ export interface BenchmarkObservation {
|
|
| 32 |
timeToFirstTokenMs: number | null;
|
| 33 |
streamedTokenEvents: number;
|
| 34 |
backendReport: BackendReport;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
export interface BenchmarkReport {
|
| 38 |
-
schemaVersion:
|
| 39 |
kind: 'bonsai-browser-benchmark';
|
| 40 |
startedAt: string;
|
| 41 |
completedAt: string;
|
|
@@ -43,6 +100,8 @@ export interface BenchmarkReport {
|
|
| 43 |
id: ManifestModelV2['id'];
|
| 44 |
displayName: string;
|
| 45 |
architecture: string;
|
|
|
|
|
|
|
| 46 |
downloadBytes: number;
|
| 47 |
contextSize: number;
|
| 48 |
};
|
|
@@ -60,11 +119,36 @@ export interface BenchmarkReport {
|
|
| 60 |
};
|
| 61 |
};
|
| 62 |
generation: {
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
elapsedMs: number | null;
|
| 69 |
timeToFirstTokenMs: number | null;
|
| 70 |
promptTokensPerSecond: number | null;
|
|
@@ -75,7 +159,7 @@ export interface BenchmarkReport {
|
|
| 75 |
completionTokens: number;
|
| 76 |
engineTotalTokens: number | null;
|
| 77 |
totalTokens: number | null;
|
| 78 |
-
tokenCountSource: '
|
| 79 |
finishReason: GenerateResult['finishReason'];
|
| 80 |
};
|
| 81 |
execution: {
|
|
@@ -141,8 +225,289 @@ export function classifyCacheState(observedBytes: number | null, totalBytes: num
|
|
| 141 |
return 'partial';
|
| 142 |
}
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
export function buildBenchmarkReport(observation: BenchmarkObservation): BenchmarkReport {
|
| 145 |
const { generationResult, capabilities, backendReport, loadResult } = observation;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
const usage = generationResult.usage;
|
| 147 |
const engineCompletionTokens = usage
|
| 148 |
&& Number.isFinite(usage.completionTokens)
|
|
@@ -152,10 +517,47 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
|
|
| 152 |
const streamedTokenEvents = Number.isFinite(observation.streamedTokenEvents)
|
| 153 |
? Math.floor(Math.max(0, observation.streamedTokenEvents))
|
| 154 |
: 0;
|
| 155 |
-
const completionTokens =
|
| 156 |
const promptTokens = usage && Number.isFinite(usage.promptTokens) && usage.promptTokens >= 0
|
| 157 |
? Math.floor(usage.promptTokens)
|
| 158 |
: null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
const engineTotalTokens = usage && Number.isFinite(usage.totalTokens) && usage.totalTokens >= 0
|
| 160 |
? Math.floor(usage.totalTokens)
|
| 161 |
: null;
|
|
@@ -172,7 +574,7 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
|
|
| 172 |
const dflashPairing = observation.manifest.dflash.pairings[observation.model.id];
|
| 173 |
|
| 174 |
return {
|
| 175 |
-
schemaVersion:
|
| 176 |
kind: 'bonsai-browser-benchmark',
|
| 177 |
startedAt: observation.startedAt,
|
| 178 |
completedAt: observation.completedAt,
|
|
@@ -180,6 +582,8 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
|
|
| 180 |
id: observation.model.id,
|
| 181 |
displayName: observation.model.displayName,
|
| 182 |
architecture: observation.model.architecture,
|
|
|
|
|
|
|
| 183 |
downloadBytes: observation.model.downloadBytes,
|
| 184 |
contextSize: observation.contextSize,
|
| 185 |
},
|
|
@@ -202,11 +606,33 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
|
|
| 202 |
},
|
| 203 |
},
|
| 204 |
generation: {
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
elapsedMs: roundedNonNegative(observation.generationElapsedMs, 1),
|
| 211 |
timeToFirstTokenMs: roundedNonNegative(observation.timeToFirstTokenMs, 1),
|
| 212 |
promptTokensPerSecond: roundedNonNegative(
|
|
@@ -220,9 +646,7 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
|
|
| 220 |
completionTokens,
|
| 221 |
engineTotalTokens,
|
| 222 |
totalTokens,
|
| 223 |
-
tokenCountSource:
|
| 224 |
-
? 'engine-usage'
|
| 225 |
-
: 'stream-events-fallback',
|
| 226 |
finishReason: generationResult.finishReason,
|
| 227 |
},
|
| 228 |
execution: {
|
|
|
|
| 1 |
+
import { sha256 } from '@noble/hashes/sha2.js';
|
| 2 |
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
| 3 |
import type {
|
| 4 |
BackendReport,
|
| 5 |
EngineCapabilities,
|
| 6 |
+
EngineChatMessage,
|
| 7 |
+
EngineSampledTokenTraceEntry,
|
| 8 |
+
GenerateParams,
|
| 9 |
GenerateResult,
|
| 10 |
LoadModelResult,
|
| 11 |
ManifestModelV2,
|
| 12 |
ModelManifestV2,
|
| 13 |
RequestedBackend,
|
| 14 |
+
ScoreSequenceResult,
|
| 15 |
} from '../engine';
|
| 16 |
+
import type { StateDriftReferenceFixture } from './state-drift-reference';
|
| 17 |
|
| 18 |
+
export const BENCHMARK_WORKLOADS = {
|
| 19 |
+
'field-core-v1': {
|
| 20 |
+
id: 'field-core-v1',
|
| 21 |
+
label: 'Field core · short',
|
| 22 |
+
description: 'Stable throughput workload; completion length remains operator-selected.',
|
| 23 |
+
requiredMaxTokens: null,
|
| 24 |
+
messages: [{
|
| 25 |
+
role: 'user',
|
| 26 |
+
content: 'Describe how a tree survives one dry season in exactly eight short, factual sentences.',
|
| 27 |
+
}],
|
| 28 |
+
},
|
| 29 |
+
'state-drift-1k-v1': {
|
| 30 |
+
id: 'state-drift-1k-v1',
|
| 31 |
+
label: 'State drift · 1K',
|
| 32 |
+
description: 'P2.5 recurrent-state evidence; requires an exact 1,024-token trace.',
|
| 33 |
+
requiredMaxTokens: 1_024,
|
| 34 |
+
messages: [{
|
| 35 |
+
role: 'user',
|
| 36 |
+
content: 'Output consecutive integers starting at 1, separated only by a comma and one space. Continue without explanation until you reach 500.',
|
| 37 |
+
}],
|
| 38 |
+
},
|
| 39 |
+
'context-prefill-8k-v1': {
|
| 40 |
+
id: 'context-prefill-8k-v1',
|
| 41 |
+
label: 'Context prefill · >8K',
|
| 42 |
+
description: 'P2.5 long-context evidence; requires more than 8,192 engine-counted prompt tokens.',
|
| 43 |
+
requiredMaxTokens: 8,
|
| 44 |
+
messages: [{
|
| 45 |
+
role: 'user',
|
| 46 |
+
content: `Continue the pattern.${' x'.repeat(8_300)}`,
|
| 47 |
+
}],
|
| 48 |
+
},
|
| 49 |
+
} as const satisfies Record<string, {
|
| 50 |
+
id: string;
|
| 51 |
+
label: string;
|
| 52 |
+
description: string;
|
| 53 |
+
requiredMaxTokens: number | null;
|
| 54 |
+
messages: readonly EngineChatMessage[];
|
| 55 |
+
}>;
|
| 56 |
+
|
| 57 |
+
export type BenchmarkWorkloadId = keyof typeof BENCHMARK_WORKLOADS;
|
| 58 |
+
export const DEFAULT_BENCHMARK_WORKLOAD_ID: BenchmarkWorkloadId = 'field-core-v1';
|
| 59 |
+
export const BENCHMARK_PROMPT_ID = DEFAULT_BENCHMARK_WORKLOAD_ID;
|
| 60 |
+
export const BENCHMARK_PROMPT = BENCHMARK_WORKLOADS[DEFAULT_BENCHMARK_WORKLOAD_ID].messages[0].content;
|
| 61 |
+
export const TOKEN_ID_CHECKPOINTS = [64, 128, 256, 512, 768, 1_024] as const;
|
| 62 |
+
export type TokenIdCheckpointSize = typeof TOKEN_ID_CHECKPOINTS[number];
|
| 63 |
|
| 64 |
export type CacheState = 'empty' | 'partial' | 'cached' | 'unknown';
|
| 65 |
|
|
|
|
| 71 |
capabilities: EngineCapabilities;
|
| 72 |
requestedBackend: RequestedBackend;
|
| 73 |
contextSize: number;
|
| 74 |
+
workloadId: BenchmarkWorkloadId;
|
| 75 |
+
generationRequest: GenerateParams;
|
| 76 |
coldLoadMs: number;
|
| 77 |
warmLoadMs: number;
|
| 78 |
coldCachedBytes: number | null;
|
|
|
|
| 83 |
timeToFirstTokenMs: number | null;
|
| 84 |
streamedTokenEvents: number;
|
| 85 |
backendReport: BackendReport;
|
| 86 |
+
teacherForcedReferenceScore?: TeacherForcedReferenceScoreEvidence | null;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export interface TeacherForcedReferenceScoreEvidence {
|
| 90 |
+
fixture: StateDriftReferenceFixture;
|
| 91 |
+
score: ScoreSequenceResult;
|
| 92 |
}
|
| 93 |
|
| 94 |
export interface BenchmarkReport {
|
| 95 |
+
schemaVersion: 3;
|
| 96 |
kind: 'bonsai-browser-benchmark';
|
| 97 |
startedAt: string;
|
| 98 |
completedAt: string;
|
|
|
|
| 100 |
id: ManifestModelV2['id'];
|
| 101 |
displayName: string;
|
| 102 |
architecture: string;
|
| 103 |
+
source: ManifestModelV2['source'];
|
| 104 |
+
shards: Array<Pick<ManifestModelV2['files'][number], 'path' | 'bytes' | 'sha256'>>;
|
| 105 |
downloadBytes: number;
|
| 106 |
contextSize: number;
|
| 107 |
};
|
|
|
|
| 119 |
};
|
| 120 |
};
|
| 121 |
generation: {
|
| 122 |
+
workload: {
|
| 123 |
+
id: BenchmarkWorkloadId;
|
| 124 |
+
messages: EngineChatMessage[];
|
| 125 |
+
};
|
| 126 |
+
sampling: {
|
| 127 |
+
maxTokens: number;
|
| 128 |
+
temperature: number;
|
| 129 |
+
topP: number | null;
|
| 130 |
+
topK: number | null;
|
| 131 |
+
minP: number | null;
|
| 132 |
+
seed: number;
|
| 133 |
+
toolChoice: GenerateParams['toolChoice'] | null;
|
| 134 |
+
cachePrompt: boolean;
|
| 135 |
+
};
|
| 136 |
+
tokenTrace: {
|
| 137 |
+
returnTokenIds: true;
|
| 138 |
+
logprobs: true;
|
| 139 |
+
topLogprobs: 5;
|
| 140 |
+
topAlternatives: 4;
|
| 141 |
+
encoding: 'uint32-le';
|
| 142 |
+
tokenIds: number[];
|
| 143 |
+
entries: EngineSampledTokenTraceEntry[];
|
| 144 |
+
checkpointPrefixes: Array<{
|
| 145 |
+
tokens: TokenIdCheckpointSize;
|
| 146 |
+
sha256: string;
|
| 147 |
+
}>;
|
| 148 |
+
};
|
| 149 |
+
teacherForcedReferenceScore: TeacherForcedReferenceScoreEvidence | null;
|
| 150 |
+
rawText: string;
|
| 151 |
+
rawReasoningText: string;
|
| 152 |
elapsedMs: number | null;
|
| 153 |
timeToFirstTokenMs: number | null;
|
| 154 |
promptTokensPerSecond: number | null;
|
|
|
|
| 159 |
completionTokens: number;
|
| 160 |
engineTotalTokens: number | null;
|
| 161 |
totalTokens: number | null;
|
| 162 |
+
tokenCountSource: 'token-id-trace';
|
| 163 |
finishReason: GenerateResult['finishReason'];
|
| 164 |
};
|
| 165 |
execution: {
|
|
|
|
| 225 |
return 'partial';
|
| 226 |
}
|
| 227 |
|
| 228 |
+
function cloneMessages(messages: readonly EngineChatMessage[]): EngineChatMessage[] {
|
| 229 |
+
return messages.map((message) => message.role === 'assistant' && message.tool_calls
|
| 230 |
+
? {
|
| 231 |
+
...message,
|
| 232 |
+
tool_calls: message.tool_calls.map((call) => ({
|
| 233 |
+
...call,
|
| 234 |
+
function: { ...call.function },
|
| 235 |
+
})),
|
| 236 |
+
}
|
| 237 |
+
: { ...message });
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
function checkpointPrefixSha256(tokenIds: readonly number[], tokens: number): string {
|
| 241 |
+
const bytes = new Uint8Array(tokens * Uint32Array.BYTES_PER_ELEMENT);
|
| 242 |
+
const view = new DataView(bytes.buffer);
|
| 243 |
+
for (let index = 0; index < tokens; index += 1) {
|
| 244 |
+
view.setUint32(index * Uint32Array.BYTES_PER_ELEMENT, tokenIds[index]!, true);
|
| 245 |
+
}
|
| 246 |
+
return bytesToHex(sha256(bytes));
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
function buildCheckpointPrefixes(tokenIds: readonly number[]): BenchmarkReport['generation']['tokenTrace']['checkpointPrefixes'] {
|
| 250 |
+
return TOKEN_ID_CHECKPOINTS
|
| 251 |
+
.filter((tokens) => tokens <= tokenIds.length)
|
| 252 |
+
.map((tokens) => ({ tokens, sha256: checkpointPrefixSha256(tokenIds, tokens) }));
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
function cloneValidatedTokenTrace(
|
| 256 |
+
value: GenerateResult['tokenTrace'],
|
| 257 |
+
tokenIds: readonly number[],
|
| 258 |
+
): EngineSampledTokenTraceEntry[] {
|
| 259 |
+
if (value === null) {
|
| 260 |
+
throw new Error('Benchmark generation completed without a sampled-token logprob trace.');
|
| 261 |
+
}
|
| 262 |
+
if (value.length !== tokenIds.length) {
|
| 263 |
+
throw new Error(
|
| 264 |
+
`Benchmark sampled-token trace length ${value.length} does not match token-id length ${tokenIds.length}.`,
|
| 265 |
+
);
|
| 266 |
+
}
|
| 267 |
+
return value.map((entry, index) => {
|
| 268 |
+
const selected = { ...entry.selected };
|
| 269 |
+
if (selected.id !== tokenIds[index]) {
|
| 270 |
+
throw new Error(`Benchmark sampled-token trace entry ${index} does not match its token id.`);
|
| 271 |
+
}
|
| 272 |
+
if (typeof selected.logprob !== 'number' || !Number.isFinite(selected.logprob)) {
|
| 273 |
+
throw new Error(`Benchmark sampled-token trace entry ${index} has an invalid selected logprob.`);
|
| 274 |
+
}
|
| 275 |
+
if (!Array.isArray(entry.topCandidates) || entry.topCandidates.length !== 5) {
|
| 276 |
+
throw new Error(`Benchmark sampled-token trace entry ${index} must contain exactly five candidates.`);
|
| 277 |
+
}
|
| 278 |
+
const seenIds = new Set<number>();
|
| 279 |
+
const topCandidates = entry.topCandidates.map((candidate, candidateIndex) => {
|
| 280 |
+
if (!Number.isSafeInteger(candidate.id) || candidate.id < 0 || candidate.id > 0xffff_ffff) {
|
| 281 |
+
throw new Error(
|
| 282 |
+
`Benchmark sampled-token trace entry ${index} candidate ${candidateIndex} has an invalid id.`,
|
| 283 |
+
);
|
| 284 |
+
}
|
| 285 |
+
if (seenIds.has(candidate.id)) {
|
| 286 |
+
throw new Error(`Benchmark sampled-token trace entry ${index} has duplicate candidate ids.`);
|
| 287 |
+
}
|
| 288 |
+
seenIds.add(candidate.id);
|
| 289 |
+
if (typeof candidate.logprob !== 'number' || !Number.isFinite(candidate.logprob)) {
|
| 290 |
+
throw new Error(
|
| 291 |
+
`Benchmark sampled-token trace entry ${index} candidate ${candidateIndex} has an invalid logprob.`,
|
| 292 |
+
);
|
| 293 |
+
}
|
| 294 |
+
const previous = entry.topCandidates[candidateIndex - 1];
|
| 295 |
+
if (previous && (
|
| 296 |
+
previous.logprob < candidate.logprob
|
| 297 |
+
|| (previous.logprob === candidate.logprob && previous.id > candidate.id)
|
| 298 |
+
)) {
|
| 299 |
+
throw new Error(`Benchmark sampled-token trace entry ${index} candidates are not sorted.`);
|
| 300 |
+
}
|
| 301 |
+
return { ...candidate };
|
| 302 |
+
});
|
| 303 |
+
const selectedCandidate = topCandidates.find((candidate) => candidate.id === selected.id);
|
| 304 |
+
if (!selectedCandidate || selectedCandidate.logprob !== selected.logprob) {
|
| 305 |
+
throw new Error(
|
| 306 |
+
`Benchmark sampled-token trace entry ${index} does not include its selected token and logprob.`,
|
| 307 |
+
);
|
| 308 |
+
}
|
| 309 |
+
return { selected, topCandidates };
|
| 310 |
+
});
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
function cloneTeacherForcedReferenceScore(
|
| 314 |
+
value: TeacherForcedReferenceScoreEvidence | null | undefined,
|
| 315 |
+
workloadId: BenchmarkWorkloadId,
|
| 316 |
+
): TeacherForcedReferenceScoreEvidence | null {
|
| 317 |
+
if (workloadId !== 'state-drift-1k-v1') {
|
| 318 |
+
if (value != null) {
|
| 319 |
+
throw new Error('Teacher-forced reference scoring is only valid for state-drift-1k-v1.');
|
| 320 |
+
}
|
| 321 |
+
return null;
|
| 322 |
+
}
|
| 323 |
+
if (value == null) {
|
| 324 |
+
throw new Error('state-drift-1k-v1 requires a complete teacher-forced CPU reference score.');
|
| 325 |
+
}
|
| 326 |
+
const { fixture, score } = value;
|
| 327 |
+
if (
|
| 328 |
+
fixture.schemaVersion !== 1
|
| 329 |
+
|| fixture.kind !== 'bonsai-state-drift-reference'
|
| 330 |
+
|| fixture.workload.id !== 'state-drift-1k-v1'
|
| 331 |
+
|| fixture.tokenEncoding !== 'uint32-le'
|
| 332 |
+
|| fixture.promptTokenIds.length !== 38
|
| 333 |
+
|| fixture.referenceTokenIds.length !== 1_024
|
| 334 |
+
) {
|
| 335 |
+
throw new Error('state-drift-1k-v1 received an invalid CPU reference fixture.');
|
| 336 |
+
}
|
| 337 |
+
const expectedMethod: ScoreSequenceResult['method'] = {
|
| 338 |
+
promptMode: 'raw-token-id-prefix',
|
| 339 |
+
maxTokensPerStep: 1,
|
| 340 |
+
temperature: 0,
|
| 341 |
+
topK: 1,
|
| 342 |
+
reportedTopLogprobs: 5,
|
| 343 |
+
logitBias: 1_000,
|
| 344 |
+
cachePromptFirst: false,
|
| 345 |
+
cachePromptSubsequent: true,
|
| 346 |
+
};
|
| 347 |
+
if (JSON.stringify(score.method) !== JSON.stringify(expectedMethod)) {
|
| 348 |
+
throw new Error('state-drift-1k-v1 teacher-forced method metadata is not locked.');
|
| 349 |
+
}
|
| 350 |
+
if (score.entries.length !== 1_024 || score.summary.tokenCount !== 1_024) {
|
| 351 |
+
throw new Error('state-drift-1k-v1 requires exactly 1,024 teacher-forced score entries.');
|
| 352 |
+
}
|
| 353 |
+
const entries = score.entries.map((entry, index) => {
|
| 354 |
+
const referenceTokenId = fixture.referenceTokenIds[index];
|
| 355 |
+
if (entry.index !== index || entry.selectedReference.id !== referenceTokenId) {
|
| 356 |
+
throw new Error(`Teacher-forced score entry ${index} does not select its CPU reference token.`);
|
| 357 |
+
}
|
| 358 |
+
if (!Number.isFinite(entry.selectedReference.logprob)) {
|
| 359 |
+
throw new Error(`Teacher-forced score entry ${index} has a non-finite reference logprob.`);
|
| 360 |
+
}
|
| 361 |
+
if (!Array.isArray(entry.topCandidates) || entry.topCandidates.length !== 5) {
|
| 362 |
+
throw new Error(`Teacher-forced score entry ${index} must contain five natural candidates.`);
|
| 363 |
+
}
|
| 364 |
+
const seenIds = new Set<number>();
|
| 365 |
+
const topCandidates = entry.topCandidates.map((candidate, candidateIndex) => {
|
| 366 |
+
if (
|
| 367 |
+
!Number.isSafeInteger(candidate.id)
|
| 368 |
+
|| candidate.id < 0
|
| 369 |
+
|| !Number.isFinite(candidate.logprob)
|
| 370 |
+
|| seenIds.has(candidate.id)
|
| 371 |
+
) {
|
| 372 |
+
throw new Error(`Teacher-forced score entry ${index} candidate ${candidateIndex} is invalid.`);
|
| 373 |
+
}
|
| 374 |
+
seenIds.add(candidate.id);
|
| 375 |
+
const previous = entry.topCandidates[candidateIndex - 1];
|
| 376 |
+
if (previous && (
|
| 377 |
+
previous.logprob < candidate.logprob
|
| 378 |
+
|| (previous.logprob === candidate.logprob && previous.id > candidate.id)
|
| 379 |
+
)) {
|
| 380 |
+
throw new Error(`Teacher-forced score entry ${index} candidates are not sorted.`);
|
| 381 |
+
}
|
| 382 |
+
return { ...candidate };
|
| 383 |
+
});
|
| 384 |
+
if (
|
| 385 |
+
entry.naturalTop1.id !== topCandidates[0]?.id
|
| 386 |
+
|| entry.naturalTop1.logprob !== topCandidates[0]?.logprob
|
| 387 |
+
) {
|
| 388 |
+
throw new Error(`Teacher-forced score entry ${index} has inconsistent natural top-1 data.`);
|
| 389 |
+
}
|
| 390 |
+
const referenceRank = topCandidates.findIndex((candidate) => candidate.id === referenceTokenId);
|
| 391 |
+
const normalizedReferenceRank = referenceRank === -1 ? null : referenceRank;
|
| 392 |
+
if (entry.referenceRankInTopCandidatesZeroBased !== normalizedReferenceRank) {
|
| 393 |
+
throw new Error(`Teacher-forced score entry ${index} has an invalid reference rank.`);
|
| 394 |
+
}
|
| 395 |
+
const margin = topCandidates[0]!.logprob - topCandidates[1]!.logprob;
|
| 396 |
+
if (!Number.isFinite(entry.top1Top2Margin) || Math.abs(entry.top1Top2Margin - margin) > 1e-12) {
|
| 397 |
+
throw new Error(`Teacher-forced score entry ${index} has an invalid top-1/top-2 margin.`);
|
| 398 |
+
}
|
| 399 |
+
return {
|
| 400 |
+
index,
|
| 401 |
+
selectedReference: { ...entry.selectedReference },
|
| 402 |
+
naturalTop1: { ...entry.naturalTop1 },
|
| 403 |
+
topCandidates,
|
| 404 |
+
referenceRankInTopCandidatesZeroBased: normalizedReferenceRank,
|
| 405 |
+
top1Top2Margin: entry.top1Top2Margin,
|
| 406 |
+
};
|
| 407 |
+
});
|
| 408 |
+
const meanNll = -entries.reduce(
|
| 409 |
+
(sum, entry) => sum + entry.selectedReference.logprob,
|
| 410 |
+
0,
|
| 411 |
+
) / entries.length;
|
| 412 |
+
const perplexity = Math.exp(meanNll);
|
| 413 |
+
if (
|
| 414 |
+
!Number.isFinite(score.summary.meanNll)
|
| 415 |
+
|| !Number.isFinite(score.summary.perplexity)
|
| 416 |
+
|| Math.abs(score.summary.meanNll - meanNll) > 1e-12
|
| 417 |
+
|| Math.abs(score.summary.perplexity - perplexity) > Math.max(1, perplexity) * 1e-12
|
| 418 |
+
) {
|
| 419 |
+
throw new Error('Teacher-forced score summary does not match its reference logprobs.');
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
return {
|
| 423 |
+
fixture: {
|
| 424 |
+
schemaVersion: 1,
|
| 425 |
+
kind: 'bonsai-state-drift-reference',
|
| 426 |
+
workload: {
|
| 427 |
+
id: 'state-drift-1k-v1',
|
| 428 |
+
messages: fixture.workload.messages.map((message) => ({ ...message })),
|
| 429 |
+
},
|
| 430 |
+
provenance: {
|
| 431 |
+
sourceEvidence: fixture.provenance.sourceEvidence,
|
| 432 |
+
engineRevision: fixture.provenance.engineRevision,
|
| 433 |
+
nativeBinary: { ...fixture.provenance.nativeBinary },
|
| 434 |
+
model: { ...fixture.provenance.model },
|
| 435 |
+
renderedPromptSha256: fixture.provenance.renderedPromptSha256,
|
| 436 |
+
execution: { ...fixture.provenance.execution },
|
| 437 |
+
},
|
| 438 |
+
tokenEncoding: 'uint32-le',
|
| 439 |
+
promptTokenIds: [...fixture.promptTokenIds],
|
| 440 |
+
promptTokenIdsSha256: fixture.promptTokenIdsSha256,
|
| 441 |
+
referenceTokenIds: [...fixture.referenceTokenIds],
|
| 442 |
+
referenceTokenIdsSha256: fixture.referenceTokenIdsSha256,
|
| 443 |
+
checkpointPrefixes: fixture.checkpointPrefixes.map((checkpoint) => ({ ...checkpoint })),
|
| 444 |
+
},
|
| 445 |
+
score: {
|
| 446 |
+
method: { ...expectedMethod },
|
| 447 |
+
entries,
|
| 448 |
+
summary: {
|
| 449 |
+
tokenCount: 1_024,
|
| 450 |
+
meanNll: score.summary.meanNll,
|
| 451 |
+
perplexity: score.summary.perplexity,
|
| 452 |
+
},
|
| 453 |
+
},
|
| 454 |
+
};
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
export function buildBenchmarkReport(observation: BenchmarkObservation): BenchmarkReport {
|
| 458 |
const { generationResult, capabilities, backendReport, loadResult } = observation;
|
| 459 |
+
const workload = BENCHMARK_WORKLOADS[observation.workloadId];
|
| 460 |
+
const generationRequest = observation.generationRequest;
|
| 461 |
+
const maxTokens = generationRequest.maxTokens ?? 512;
|
| 462 |
+
const temperature = generationRequest.temperature ?? 0;
|
| 463 |
+
const topP = generationRequest.topP ?? null;
|
| 464 |
+
const topK = generationRequest.topK ?? 1;
|
| 465 |
+
const minP = generationRequest.minP ?? null;
|
| 466 |
+
const seed = generationRequest.seed ?? 42;
|
| 467 |
+
const toolChoice = generationRequest.toolChoice ?? null;
|
| 468 |
+
const cachePrompt = generationRequest.cachePrompt ?? true;
|
| 469 |
+
if (generationRequest.returnTokenIds !== true) {
|
| 470 |
+
throw new Error('Benchmark reports require returnTokenIds=true.');
|
| 471 |
+
}
|
| 472 |
+
if (JSON.stringify(generationRequest.messages) !== JSON.stringify(workload.messages)) {
|
| 473 |
+
throw new Error(`Benchmark messages do not match workload ${workload.id}.`);
|
| 474 |
+
}
|
| 475 |
+
if (generationResult.tokenIds === null) {
|
| 476 |
+
throw new Error('Benchmark generation completed without a token-id trace.');
|
| 477 |
+
}
|
| 478 |
+
const tokenIds = [...generationResult.tokenIds];
|
| 479 |
+
for (const [index, tokenId] of tokenIds.entries()) {
|
| 480 |
+
if (!Number.isSafeInteger(tokenId) || tokenId < 0 || tokenId > 0xffff_ffff) {
|
| 481 |
+
throw new Error(`Benchmark token id ${index} is invalid.`);
|
| 482 |
+
}
|
| 483 |
+
}
|
| 484 |
+
const tokenTrace = cloneValidatedTokenTrace(generationResult.tokenTrace, tokenIds);
|
| 485 |
+
if (typeof generationResult.reasoningText !== 'string') {
|
| 486 |
+
throw new Error('Benchmark generation completed without a reasoning-text channel.');
|
| 487 |
+
}
|
| 488 |
+
const checkpointPrefixes = buildCheckpointPrefixes(tokenIds);
|
| 489 |
+
if (workload.requiredMaxTokens !== null) {
|
| 490 |
+
if (
|
| 491 |
+
maxTokens !== workload.requiredMaxTokens
|
| 492 |
+
|| temperature !== 0
|
| 493 |
+
|| topP !== 1
|
| 494 |
+
|| topK !== 1
|
| 495 |
+
|| minP !== null
|
| 496 |
+
|| seed !== 42
|
| 497 |
+
|| toolChoice !== 'none'
|
| 498 |
+
|| cachePrompt !== false
|
| 499 |
+
) {
|
| 500 |
+
throw new Error(`${workload.id} requires its locked greedy sampling configuration.`);
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
if (workload.id === 'state-drift-1k-v1') {
|
| 504 |
+
if (tokenIds.length !== workload.requiredMaxTokens || checkpointPrefixes.length !== TOKEN_ID_CHECKPOINTS.length) {
|
| 505 |
+
throw new Error(`${workload.id} requires exactly 1,024 sampled token ids and all checkpoint hashes.`);
|
| 506 |
+
}
|
| 507 |
+
if (generationResult.finishReason !== 'length') {
|
| 508 |
+
throw new Error(`${workload.id} requires finishReason=length after exhausting its 1,024-token budget.`);
|
| 509 |
+
}
|
| 510 |
+
}
|
| 511 |
const usage = generationResult.usage;
|
| 512 |
const engineCompletionTokens = usage
|
| 513 |
&& Number.isFinite(usage.completionTokens)
|
|
|
|
| 517 |
const streamedTokenEvents = Number.isFinite(observation.streamedTokenEvents)
|
| 518 |
? Math.floor(Math.max(0, observation.streamedTokenEvents))
|
| 519 |
: 0;
|
| 520 |
+
const completionTokens = tokenIds.length;
|
| 521 |
const promptTokens = usage && Number.isFinite(usage.promptTokens) && usage.promptTokens >= 0
|
| 522 |
? Math.floor(usage.promptTokens)
|
| 523 |
: null;
|
| 524 |
+
if (workload.id === 'context-prefill-8k-v1') {
|
| 525 |
+
const tuning = loadResult.tuning;
|
| 526 |
+
const exactRuntimePolicy = observation.model.id === '27b'
|
| 527 |
+
&& loadResult.modelId === '27b'
|
| 528 |
+
&& observation.contextSize === 8_448
|
| 529 |
+
&& loadResult.context.size === 8_448
|
| 530 |
+
&& observation.requestedBackend === 'webgpu'
|
| 531 |
+
&& loadResult.backend === 'webgpu'
|
| 532 |
+
&& tuning.scope === 'benchmark'
|
| 533 |
+
&& tuning.requested.flashMode === 'auto'
|
| 534 |
+
&& tuning.requested.cacheTypeK === 'q4_0'
|
| 535 |
+
&& tuning.requested.cacheTypeV === 'q4_0'
|
| 536 |
+
&& tuning.observed.flashAttention === true
|
| 537 |
+
&& tuning.observed.cacheTypeK === 'q4_0'
|
| 538 |
+
&& tuning.observed.cacheTypeV === 'q4_0'
|
| 539 |
+
&& tuning.observed.kvBufferBytes !== null
|
| 540 |
+
&& tuning.observed.kvBufferBytes > 0
|
| 541 |
+
&& tuning.applied;
|
| 542 |
+
if (!exactRuntimePolicy) {
|
| 543 |
+
throw new Error(
|
| 544 |
+
`${workload.id} requires Bonsai 27B at context 8,448 with explicit WebGPU benchmark Flash auto and applied Q4_0 K/V.`,
|
| 545 |
+
);
|
| 546 |
+
}
|
| 547 |
+
if (tokenIds.length !== 8) {
|
| 548 |
+
throw new Error(`${workload.id} requires exactly 8 sampled token ids.`);
|
| 549 |
+
}
|
| 550 |
+
if (generationResult.finishReason !== 'length') {
|
| 551 |
+
throw new Error(`${workload.id} requires finishReason=length after exhausting its 8-token budget.`);
|
| 552 |
+
}
|
| 553 |
+
if (promptTokens === null || promptTokens <= 8_192) {
|
| 554 |
+
throw new Error(`${workload.id} requires more than 8,192 engine-counted prompt tokens.`);
|
| 555 |
+
}
|
| 556 |
+
}
|
| 557 |
+
const teacherForcedReferenceScore = cloneTeacherForcedReferenceScore(
|
| 558 |
+
observation.teacherForcedReferenceScore,
|
| 559 |
+
workload.id,
|
| 560 |
+
);
|
| 561 |
const engineTotalTokens = usage && Number.isFinite(usage.totalTokens) && usage.totalTokens >= 0
|
| 562 |
? Math.floor(usage.totalTokens)
|
| 563 |
: null;
|
|
|
|
| 574 |
const dflashPairing = observation.manifest.dflash.pairings[observation.model.id];
|
| 575 |
|
| 576 |
return {
|
| 577 |
+
schemaVersion: 3,
|
| 578 |
kind: 'bonsai-browser-benchmark',
|
| 579 |
startedAt: observation.startedAt,
|
| 580 |
completedAt: observation.completedAt,
|
|
|
|
| 582 |
id: observation.model.id,
|
| 583 |
displayName: observation.model.displayName,
|
| 584 |
architecture: observation.model.architecture,
|
| 585 |
+
source: { ...observation.model.source },
|
| 586 |
+
shards: observation.model.files.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 })),
|
| 587 |
downloadBytes: observation.model.downloadBytes,
|
| 588 |
contextSize: observation.contextSize,
|
| 589 |
},
|
|
|
|
| 606 |
},
|
| 607 |
},
|
| 608 |
generation: {
|
| 609 |
+
workload: {
|
| 610 |
+
id: workload.id,
|
| 611 |
+
messages: cloneMessages(generationRequest.messages),
|
| 612 |
+
},
|
| 613 |
+
sampling: {
|
| 614 |
+
maxTokens,
|
| 615 |
+
temperature,
|
| 616 |
+
topP,
|
| 617 |
+
topK,
|
| 618 |
+
minP,
|
| 619 |
+
seed,
|
| 620 |
+
toolChoice,
|
| 621 |
+
cachePrompt,
|
| 622 |
+
},
|
| 623 |
+
tokenTrace: {
|
| 624 |
+
returnTokenIds: true,
|
| 625 |
+
logprobs: true,
|
| 626 |
+
topLogprobs: 5,
|
| 627 |
+
topAlternatives: 4,
|
| 628 |
+
encoding: 'uint32-le',
|
| 629 |
+
tokenIds,
|
| 630 |
+
entries: tokenTrace,
|
| 631 |
+
checkpointPrefixes,
|
| 632 |
+
},
|
| 633 |
+
teacherForcedReferenceScore,
|
| 634 |
+
rawText: generationResult.text,
|
| 635 |
+
rawReasoningText: generationResult.reasoningText,
|
| 636 |
elapsedMs: roundedNonNegative(observation.generationElapsedMs, 1),
|
| 637 |
timeToFirstTokenMs: roundedNonNegative(observation.timeToFirstTokenMs, 1),
|
| 638 |
promptTokensPerSecond: roundedNonNegative(
|
|
|
|
| 646 |
completionTokens,
|
| 647 |
engineTotalTokens,
|
| 648 |
totalTokens,
|
| 649 |
+
tokenCountSource: 'token-id-trace',
|
|
|
|
|
|
|
| 650 |
finishReason: generationResult.finishReason,
|
| 651 |
},
|
| 652 |
execution: {
|
src/bench/state-drift-reference.test.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from 'vitest';
|
| 2 |
+
import fixtureJson from '../../public/fixtures/state-drift-27b-cpu-reference.json';
|
| 3 |
+
import type {
|
| 4 |
+
EngineCapabilities,
|
| 5 |
+
LoadModelResult,
|
| 6 |
+
ManifestModelV2,
|
| 7 |
+
} from '../engine';
|
| 8 |
+
import { validateStateDriftReferenceFixture } from './state-drift-reference';
|
| 9 |
+
|
| 10 |
+
const fixture: unknown = fixtureJson;
|
| 11 |
+
|
| 12 |
+
const model = {
|
| 13 |
+
id: '27b',
|
| 14 |
+
source: {
|
| 15 |
+
repo: 'WaveCut/Bonsai-web-GGUF',
|
| 16 |
+
revision: 'd85382fa09fe868c0242d81488dfc2edd8d3729b',
|
| 17 |
+
file: 'Bonsai-27B-Q1_0.gguf',
|
| 18 |
+
bytes: 3_803_452_480,
|
| 19 |
+
sha256: '17ef842e47450caeb8eaa3ebfbbab5d2f2278b62b79be107985fb69a2f819aa0',
|
| 20 |
+
},
|
| 21 |
+
} as ManifestModelV2;
|
| 22 |
+
|
| 23 |
+
const capabilities = {
|
| 24 |
+
runtime: {
|
| 25 |
+
llamaCppRevision: '00fa7cb284cbf133fc426733bd64238a3588a33e',
|
| 26 |
+
},
|
| 27 |
+
} as EngineCapabilities;
|
| 28 |
+
|
| 29 |
+
const loadResult = {
|
| 30 |
+
modelId: '27b',
|
| 31 |
+
backend: 'webgpu',
|
| 32 |
+
gate: { requestedBackend: 'webgpu' },
|
| 33 |
+
context: {
|
| 34 |
+
size: 2_048,
|
| 35 |
+
batchSize: 32,
|
| 36 |
+
microBatchSize: 16,
|
| 37 |
+
vocabularySize: 248_320,
|
| 38 |
+
},
|
| 39 |
+
tuning: {
|
| 40 |
+
scope: 'benchmark',
|
| 41 |
+
requested: { nBatch: 32, nUbatch: 16 },
|
| 42 |
+
observed: { nBatch: 32, nUbatch: 16 },
|
| 43 |
+
applied: true,
|
| 44 |
+
},
|
| 45 |
+
} as LoadModelResult;
|
| 46 |
+
|
| 47 |
+
describe('state-drift CPU reference fixture', () => {
|
| 48 |
+
it('validates the pinned fixture against the loaded model, runtime, context, and batching', () => {
|
| 49 |
+
const result = validateStateDriftReferenceFixture(fixture, {
|
| 50 |
+
model,
|
| 51 |
+
capabilities,
|
| 52 |
+
loadResult,
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
expect(result.promptTokenIds).toHaveLength(38);
|
| 56 |
+
expect(result.referenceTokenIds).toHaveLength(1_024);
|
| 57 |
+
expect(result.referenceTokenIdsSha256).toBe(
|
| 58 |
+
'c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b',
|
| 59 |
+
);
|
| 60 |
+
expect(result.checkpointPrefixes).toHaveLength(6);
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
it('rejects provenance or token-hash drift before scoring', () => {
|
| 64 |
+
const wrongModel = structuredClone(fixture) as Record<string, unknown>;
|
| 65 |
+
const provenance = wrongModel.provenance as { model: { sha256: string } };
|
| 66 |
+
provenance.model.sha256 = '0'.repeat(64);
|
| 67 |
+
expect(() => validateStateDriftReferenceFixture(wrongModel, {
|
| 68 |
+
model,
|
| 69 |
+
capabilities,
|
| 70 |
+
loadResult,
|
| 71 |
+
})).toThrow('model source sha256');
|
| 72 |
+
|
| 73 |
+
const wrongTokens = structuredClone(fixture) as { referenceTokenIds: number[] };
|
| 74 |
+
wrongTokens.referenceTokenIds[29] = wrongTokens.referenceTokenIds[29]! + 1;
|
| 75 |
+
expect(() => validateStateDriftReferenceFixture(wrongTokens, {
|
| 76 |
+
model,
|
| 77 |
+
capabilities,
|
| 78 |
+
loadResult,
|
| 79 |
+
})).toThrow('referenceTokenIdsSha256');
|
| 80 |
+
});
|
| 81 |
+
});
|
src/bench/state-drift-reference.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { sha256 } from '@noble/hashes/sha2.js';
|
| 2 |
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
| 3 |
+
import type {
|
| 4 |
+
EngineCapabilities,
|
| 5 |
+
LoadModelResult,
|
| 6 |
+
ManifestModelV2,
|
| 7 |
+
} from '../engine';
|
| 8 |
+
import { BENCHMARK_WORKLOADS, TOKEN_ID_CHECKPOINTS } from './report';
|
| 9 |
+
|
| 10 |
+
export const STATE_DRIFT_REFERENCE_PATH = 'fixtures/state-drift-27b-cpu-reference.json';
|
| 11 |
+
const STATE_DRIFT_SOURCE_EVIDENCE = 'results/space-model/27b-native-cpu-state-drift-1024-b32-u16.json';
|
| 12 |
+
const STATE_DRIFT_RENDERED_PROMPT_SHA256 = '7c46d0de443e6f1235ddaa7c0a55c9da710eb73958d5671247b6f0e3b7d189c7';
|
| 13 |
+
const STATE_DRIFT_PROMPT_TOKEN_IDS_SHA256 = 'e7af7d66c96f24cb148575bd45cef079920669eb0d193fdc830ce61dfec85451';
|
| 14 |
+
const STATE_DRIFT_REFERENCE_TOKEN_IDS_SHA256 = 'c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b';
|
| 15 |
+
|
| 16 |
+
export interface StateDriftReferenceFixture {
|
| 17 |
+
schemaVersion: 1;
|
| 18 |
+
kind: 'bonsai-state-drift-reference';
|
| 19 |
+
workload: {
|
| 20 |
+
id: 'state-drift-1k-v1';
|
| 21 |
+
messages: Array<{ role: 'user'; content: string }>;
|
| 22 |
+
};
|
| 23 |
+
provenance: {
|
| 24 |
+
sourceEvidence: string;
|
| 25 |
+
engineRevision: string;
|
| 26 |
+
nativeBinary: { bytes: number; sha256: string };
|
| 27 |
+
model: { file: string; bytes: number; sha256: string };
|
| 28 |
+
renderedPromptSha256: string;
|
| 29 |
+
execution: {
|
| 30 |
+
backend: 'cpu';
|
| 31 |
+
contextSize: number;
|
| 32 |
+
batchSize: number;
|
| 33 |
+
microBatchSize: number;
|
| 34 |
+
};
|
| 35 |
+
};
|
| 36 |
+
tokenEncoding: 'uint32-le';
|
| 37 |
+
promptTokenIds: number[];
|
| 38 |
+
promptTokenIdsSha256: string;
|
| 39 |
+
referenceTokenIds: number[];
|
| 40 |
+
referenceTokenIdsSha256: string;
|
| 41 |
+
checkpointPrefixes: Array<{ tokens: number; sha256: string }>;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export interface StateDriftReferenceContext {
|
| 45 |
+
model: ManifestModelV2;
|
| 46 |
+
capabilities: EngineCapabilities;
|
| 47 |
+
loadResult: LoadModelResult;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function fail(message: string): never {
|
| 51 |
+
throw new Error(`Invalid state-drift CPU reference: ${message}`);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function record(value: unknown, label: string): Record<string, unknown> {
|
| 55 |
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
| 56 |
+
fail(`${label} must be an object.`);
|
| 57 |
+
}
|
| 58 |
+
return value as Record<string, unknown>;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function exact<T>(value: unknown, expected: T, label: string): T {
|
| 62 |
+
if (!Object.is(value, expected)) {
|
| 63 |
+
fail(`${label} must be ${JSON.stringify(expected)}; received ${JSON.stringify(value)}.`);
|
| 64 |
+
}
|
| 65 |
+
return expected;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function nonEmptyString(value: unknown, label: string): string {
|
| 69 |
+
if (typeof value !== 'string' || value.length === 0) fail(`${label} must be a non-empty string.`);
|
| 70 |
+
return value;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function positiveInteger(value: unknown, label: string): number {
|
| 74 |
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
|
| 75 |
+
fail(`${label} must be a positive safe integer.`);
|
| 76 |
+
}
|
| 77 |
+
return value as number;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function sha256Digest(value: unknown, label: string): string {
|
| 81 |
+
const digest = nonEmptyString(value, label).toLowerCase();
|
| 82 |
+
if (!/^[0-9a-f]{64}$/.test(digest)) fail(`${label} must be a 64-character SHA256 digest.`);
|
| 83 |
+
return digest;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function gitRevision(value: unknown, label: string): string {
|
| 87 |
+
const revision = nonEmptyString(value, label).toLowerCase();
|
| 88 |
+
if (!/^[0-9a-f]{40}$/.test(revision)) fail(`${label} must be a pinned 40-character revision.`);
|
| 89 |
+
return revision;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function tokenIds(
|
| 93 |
+
value: unknown,
|
| 94 |
+
expectedLength: number,
|
| 95 |
+
vocabularySize: number,
|
| 96 |
+
label: string,
|
| 97 |
+
): number[] {
|
| 98 |
+
if (!Array.isArray(value) || value.length !== expectedLength) {
|
| 99 |
+
fail(`${label} must contain exactly ${expectedLength} token ids.`);
|
| 100 |
+
}
|
| 101 |
+
return value.map((tokenId, index) => {
|
| 102 |
+
if (!Number.isSafeInteger(tokenId) || tokenId < 0 || tokenId >= vocabularySize) {
|
| 103 |
+
fail(`${label}[${index}] is outside the loaded vocabulary.`);
|
| 104 |
+
}
|
| 105 |
+
return tokenId;
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function tokenIdsSha256(values: readonly number[], count = values.length): string {
|
| 110 |
+
const bytes = new Uint8Array(count * Uint32Array.BYTES_PER_ELEMENT);
|
| 111 |
+
const view = new DataView(bytes.buffer);
|
| 112 |
+
for (let index = 0; index < count; index += 1) {
|
| 113 |
+
view.setUint32(index * Uint32Array.BYTES_PER_ELEMENT, values[index]!, true);
|
| 114 |
+
}
|
| 115 |
+
return bytesToHex(sha256(bytes));
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export function validateStateDriftReferenceFixture(
|
| 119 |
+
value: unknown,
|
| 120 |
+
context: StateDriftReferenceContext,
|
| 121 |
+
): StateDriftReferenceFixture {
|
| 122 |
+
const root = record(value, 'root');
|
| 123 |
+
exact(root.schemaVersion, 1, 'schemaVersion');
|
| 124 |
+
exact(root.kind, 'bonsai-state-drift-reference', 'kind');
|
| 125 |
+
|
| 126 |
+
const workload = record(root.workload, 'workload');
|
| 127 |
+
exact(workload.id, 'state-drift-1k-v1', 'workload.id');
|
| 128 |
+
const expectedMessages = BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages;
|
| 129 |
+
if (JSON.stringify(workload.messages) !== JSON.stringify(expectedMessages)) {
|
| 130 |
+
fail('workload.messages do not match the locked state-drift prompt.');
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
const { model, capabilities, loadResult } = context;
|
| 134 |
+
if (
|
| 135 |
+
model.id !== '27b'
|
| 136 |
+
|| loadResult.modelId !== '27b'
|
| 137 |
+
|| loadResult.backend !== 'webgpu'
|
| 138 |
+
|| loadResult.gate.requestedBackend !== 'webgpu'
|
| 139 |
+
|| loadResult.tuning.scope !== 'benchmark'
|
| 140 |
+
|| !loadResult.tuning.applied
|
| 141 |
+
) {
|
| 142 |
+
fail('the loaded runtime is not the explicit 27B WebGPU benchmark path.');
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
const provenance = record(root.provenance, 'provenance');
|
| 146 |
+
const sourceEvidence = nonEmptyString(provenance.sourceEvidence, 'provenance.sourceEvidence');
|
| 147 |
+
exact(sourceEvidence, STATE_DRIFT_SOURCE_EVIDENCE, 'provenance.sourceEvidence');
|
| 148 |
+
const engineRevision = gitRevision(provenance.engineRevision, 'provenance.engineRevision');
|
| 149 |
+
exact(engineRevision, capabilities.runtime.llamaCppRevision, 'runtime llama.cpp revision');
|
| 150 |
+
|
| 151 |
+
const nativeBinary = record(provenance.nativeBinary, 'provenance.nativeBinary');
|
| 152 |
+
const nativeBinaryBytes = positiveInteger(nativeBinary.bytes, 'provenance.nativeBinary.bytes');
|
| 153 |
+
const nativeBinarySha256 = sha256Digest(
|
| 154 |
+
nativeBinary.sha256,
|
| 155 |
+
'provenance.nativeBinary.sha256',
|
| 156 |
+
);
|
| 157 |
+
|
| 158 |
+
const fixtureModel = record(provenance.model, 'provenance.model');
|
| 159 |
+
const modelFile = nonEmptyString(fixtureModel.file, 'provenance.model.file');
|
| 160 |
+
const modelBytes = positiveInteger(fixtureModel.bytes, 'provenance.model.bytes');
|
| 161 |
+
const modelSha256 = sha256Digest(fixtureModel.sha256, 'provenance.model.sha256');
|
| 162 |
+
exact(modelFile, model.source.file, 'model source file');
|
| 163 |
+
exact(modelBytes, model.source.bytes, 'model source bytes');
|
| 164 |
+
exact(modelSha256, model.source.sha256, 'model source sha256');
|
| 165 |
+
|
| 166 |
+
const renderedPromptSha256 = sha256Digest(
|
| 167 |
+
provenance.renderedPromptSha256,
|
| 168 |
+
'provenance.renderedPromptSha256',
|
| 169 |
+
);
|
| 170 |
+
exact(
|
| 171 |
+
renderedPromptSha256,
|
| 172 |
+
STATE_DRIFT_RENDERED_PROMPT_SHA256,
|
| 173 |
+
'provenance.renderedPromptSha256',
|
| 174 |
+
);
|
| 175 |
+
const execution = record(provenance.execution, 'provenance.execution');
|
| 176 |
+
exact(execution.backend, 'cpu', 'provenance.execution.backend');
|
| 177 |
+
const contextSize = positiveInteger(execution.contextSize, 'provenance.execution.contextSize');
|
| 178 |
+
const batchSize = positiveInteger(execution.batchSize, 'provenance.execution.batchSize');
|
| 179 |
+
const microBatchSize = positiveInteger(
|
| 180 |
+
execution.microBatchSize,
|
| 181 |
+
'provenance.execution.microBatchSize',
|
| 182 |
+
);
|
| 183 |
+
exact(contextSize, loadResult.context.size, 'reference/loaded context size');
|
| 184 |
+
exact(batchSize, loadResult.context.batchSize, 'reference/loaded batch size');
|
| 185 |
+
exact(microBatchSize, loadResult.context.microBatchSize, 'reference/loaded micro-batch size');
|
| 186 |
+
exact(loadResult.tuning.requested.nBatch, batchSize, 'requested reference batch size');
|
| 187 |
+
exact(loadResult.tuning.requested.nUbatch, microBatchSize, 'requested reference micro-batch size');
|
| 188 |
+
exact(loadResult.tuning.observed.nBatch, batchSize, 'observed reference batch size');
|
| 189 |
+
exact(loadResult.tuning.observed.nUbatch, microBatchSize, 'observed reference micro-batch size');
|
| 190 |
+
|
| 191 |
+
exact(root.tokenEncoding, 'uint32-le', 'tokenEncoding');
|
| 192 |
+
const promptTokenIds = tokenIds(
|
| 193 |
+
root.promptTokenIds,
|
| 194 |
+
38,
|
| 195 |
+
loadResult.context.vocabularySize,
|
| 196 |
+
'promptTokenIds',
|
| 197 |
+
);
|
| 198 |
+
const promptTokenIdsSha256 = sha256Digest(root.promptTokenIdsSha256, 'promptTokenIdsSha256');
|
| 199 |
+
exact(promptTokenIdsSha256, tokenIdsSha256(promptTokenIds), 'promptTokenIdsSha256');
|
| 200 |
+
exact(
|
| 201 |
+
promptTokenIdsSha256,
|
| 202 |
+
STATE_DRIFT_PROMPT_TOKEN_IDS_SHA256,
|
| 203 |
+
'pinned promptTokenIdsSha256',
|
| 204 |
+
);
|
| 205 |
+
const referenceTokenIds = tokenIds(
|
| 206 |
+
root.referenceTokenIds,
|
| 207 |
+
1_024,
|
| 208 |
+
loadResult.context.vocabularySize,
|
| 209 |
+
'referenceTokenIds',
|
| 210 |
+
);
|
| 211 |
+
const referenceTokenIdsSha256 = sha256Digest(
|
| 212 |
+
root.referenceTokenIdsSha256,
|
| 213 |
+
'referenceTokenIdsSha256',
|
| 214 |
+
);
|
| 215 |
+
exact(referenceTokenIdsSha256, tokenIdsSha256(referenceTokenIds), 'referenceTokenIdsSha256');
|
| 216 |
+
exact(
|
| 217 |
+
referenceTokenIdsSha256,
|
| 218 |
+
STATE_DRIFT_REFERENCE_TOKEN_IDS_SHA256,
|
| 219 |
+
'pinned referenceTokenIdsSha256',
|
| 220 |
+
);
|
| 221 |
+
|
| 222 |
+
if (!Array.isArray(root.checkpointPrefixes)
|
| 223 |
+
|| root.checkpointPrefixes.length !== TOKEN_ID_CHECKPOINTS.length) {
|
| 224 |
+
fail(`checkpointPrefixes must contain exactly ${TOKEN_ID_CHECKPOINTS.length} entries.`);
|
| 225 |
+
}
|
| 226 |
+
const checkpointPrefixes = root.checkpointPrefixes.map((rawCheckpoint, index) => {
|
| 227 |
+
const checkpoint = record(rawCheckpoint, `checkpointPrefixes[${index}]`);
|
| 228 |
+
const tokens = TOKEN_ID_CHECKPOINTS[index]!;
|
| 229 |
+
exact(checkpoint.tokens, tokens, `checkpointPrefixes[${index}].tokens`);
|
| 230 |
+
const embeddedSha256 = sha256Digest(
|
| 231 |
+
checkpoint.sha256,
|
| 232 |
+
`checkpointPrefixes[${index}].sha256`,
|
| 233 |
+
);
|
| 234 |
+
exact(
|
| 235 |
+
embeddedSha256,
|
| 236 |
+
tokenIdsSha256(referenceTokenIds, tokens),
|
| 237 |
+
`checkpointPrefixes[${index}].sha256`,
|
| 238 |
+
);
|
| 239 |
+
return { tokens, sha256: embeddedSha256 };
|
| 240 |
+
});
|
| 241 |
+
|
| 242 |
+
return {
|
| 243 |
+
schemaVersion: 1,
|
| 244 |
+
kind: 'bonsai-state-drift-reference',
|
| 245 |
+
workload: {
|
| 246 |
+
id: 'state-drift-1k-v1',
|
| 247 |
+
messages: expectedMessages.map((message) => ({ ...message })) as Array<{
|
| 248 |
+
role: 'user';
|
| 249 |
+
content: string;
|
| 250 |
+
}>,
|
| 251 |
+
},
|
| 252 |
+
provenance: {
|
| 253 |
+
sourceEvidence,
|
| 254 |
+
engineRevision,
|
| 255 |
+
nativeBinary: { bytes: nativeBinaryBytes, sha256: nativeBinarySha256 },
|
| 256 |
+
model: { file: modelFile, bytes: modelBytes, sha256: modelSha256 },
|
| 257 |
+
renderedPromptSha256,
|
| 258 |
+
execution: {
|
| 259 |
+
backend: 'cpu',
|
| 260 |
+
contextSize,
|
| 261 |
+
batchSize,
|
| 262 |
+
microBatchSize,
|
| 263 |
+
},
|
| 264 |
+
},
|
| 265 |
+
tokenEncoding: 'uint32-le',
|
| 266 |
+
promptTokenIds,
|
| 267 |
+
promptTokenIdsSha256,
|
| 268 |
+
referenceTokenIds,
|
| 269 |
+
referenceTokenIdsSha256,
|
| 270 |
+
checkpointPrefixes,
|
| 271 |
+
};
|
| 272 |
+
}
|
src/engine/client.test.ts
CHANGED
|
@@ -231,6 +231,50 @@ describe('BrowserEngineClient worker lifecycle', () => {
|
|
| 231 |
client.close();
|
| 232 |
});
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
it('invalidates the model when generation ignores abort beyond the grace period', async () => {
|
| 235 |
vi.useFakeTimers();
|
| 236 |
try {
|
|
|
|
| 231 |
client.close();
|
| 232 |
});
|
| 233 |
|
| 234 |
+
it('routes and softly aborts diagnostic sequence scoring', async () => {
|
| 235 |
+
const client = new BrowserEngineClient();
|
| 236 |
+
const worker = latestWorker();
|
| 237 |
+
const controller = new AbortController();
|
| 238 |
+
const scoring = client.scoreSequence({
|
| 239 |
+
promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
|
| 240 |
+
referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
|
| 241 |
+
topK: 5,
|
| 242 |
+
}, {
|
| 243 |
+
requestId: 'soft-score-sequence',
|
| 244 |
+
signal: controller.signal,
|
| 245 |
+
});
|
| 246 |
+
const scoringFailure = expect(scoring).rejects.toMatchObject({ code: 'ABORTED' });
|
| 247 |
+
|
| 248 |
+
expect(worker.postMessage).toHaveBeenCalledWith(expect.objectContaining({
|
| 249 |
+
requestId: 'soft-score-sequence',
|
| 250 |
+
method: 'scoreSequence',
|
| 251 |
+
}));
|
| 252 |
+
controller.abort();
|
| 253 |
+
const abortRequest = worker.postMessage.mock.calls
|
| 254 |
+
.map((call) => call[0] as { requestId: string; method: string })
|
| 255 |
+
.find((request) => request.method === 'abort');
|
| 256 |
+
expect(abortRequest).toBeTruthy();
|
| 257 |
+
worker.send({
|
| 258 |
+
type: 'response',
|
| 259 |
+
requestId: 'soft-score-sequence',
|
| 260 |
+
method: 'scoreSequence',
|
| 261 |
+
ok: false,
|
| 262 |
+
error: { code: 'ABORTED', message: 'Sequence scoring stopped.' },
|
| 263 |
+
});
|
| 264 |
+
if (!abortRequest) throw new Error('Expected abort request.');
|
| 265 |
+
worker.send({
|
| 266 |
+
type: 'response',
|
| 267 |
+
requestId: abortRequest.requestId,
|
| 268 |
+
method: 'abort',
|
| 269 |
+
ok: true,
|
| 270 |
+
result: { targetRequestId: 'soft-score-sequence', aborted: true },
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
await scoringFailure;
|
| 274 |
+
expect(worker.terminate).not.toHaveBeenCalled();
|
| 275 |
+
client.close();
|
| 276 |
+
});
|
| 277 |
+
|
| 278 |
it('invalidates the model when generation ignores abort beyond the grace period', async () => {
|
| 279 |
vi.useFakeTimers();
|
| 280 |
try {
|
src/engine/client.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
| 9 |
EngineWorkerMessage,
|
| 10 |
GenerateParams,
|
| 11 |
LoadModelParams,
|
|
|
|
| 12 |
} from './protocol';
|
| 13 |
|
| 14 |
type ProgressEvent = Extract<EngineEvent, { event: 'progress' }>;
|
|
@@ -134,16 +135,20 @@ export class BrowserEngineClient {
|
|
| 134 |
));
|
| 135 |
}
|
| 136 |
}, LOAD_ABORT_CLEANUP_TIMEOUT_MS);
|
| 137 |
-
} else if (method === 'generate') {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
pending.abortFallbackTimer = setTimeout(() => {
|
| 139 |
if (this.pending.has(requestId)) {
|
| 140 |
this.restart(new EngineClientError({
|
| 141 |
code: 'ENGINE_WORKER_FAILED',
|
| 142 |
-
message:
|
| 143 |
details: {
|
| 144 |
recoverable: true,
|
| 145 |
nextAction: 'reload-model',
|
| 146 |
-
cause
|
| 147 |
graceMs: GENERATION_ABORT_GRACE_MS,
|
| 148 |
},
|
| 149 |
}));
|
|
@@ -176,6 +181,10 @@ export class BrowserEngineClient {
|
|
| 176 |
return this.request('generate', params, options);
|
| 177 |
}
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
abort(targetRequestId: string) {
|
| 180 |
return this.request('abort', { targetRequestId });
|
| 181 |
}
|
|
|
|
| 9 |
EngineWorkerMessage,
|
| 10 |
GenerateParams,
|
| 11 |
LoadModelParams,
|
| 12 |
+
ScoreSequenceParams,
|
| 13 |
} from './protocol';
|
| 14 |
|
| 15 |
type ProgressEvent = Extract<EngineEvent, { event: 'progress' }>;
|
|
|
|
| 135 |
));
|
| 136 |
}
|
| 137 |
}, LOAD_ABORT_CLEANUP_TIMEOUT_MS);
|
| 138 |
+
} else if (method === 'generate' || method === 'scoreSequence') {
|
| 139 |
+
const operationLabel = method === 'generate' ? 'Generation' : 'Sequence scoring';
|
| 140 |
+
const cause = method === 'generate'
|
| 141 |
+
? 'generation-abort-timeout'
|
| 142 |
+
: 'score-sequence-abort-timeout';
|
| 143 |
pending.abortFallbackTimer = setTimeout(() => {
|
| 144 |
if (this.pending.has(requestId)) {
|
| 145 |
this.restart(new EngineClientError({
|
| 146 |
code: 'ENGINE_WORKER_FAILED',
|
| 147 |
+
message: `${operationLabel} did not stop within 5 seconds. The browser engine worker was restarted and the loaded model was invalidated.`,
|
| 148 |
details: {
|
| 149 |
recoverable: true,
|
| 150 |
nextAction: 'reload-model',
|
| 151 |
+
cause,
|
| 152 |
graceMs: GENERATION_ABORT_GRACE_MS,
|
| 153 |
},
|
| 154 |
}));
|
|
|
|
| 181 |
return this.request('generate', params, options);
|
| 182 |
}
|
| 183 |
|
| 184 |
+
scoreSequence(params: ScoreSequenceParams, options?: EngineRequestOptions) {
|
| 185 |
+
return this.request('scoreSequence', params, options);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
abort(targetRequestId: string) {
|
| 189 |
return this.request('abort', { targetRequestId });
|
| 190 |
}
|
src/engine/device-gate.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { afterEach, describe, expect, it, vi } from 'vitest';
|
| 2 |
import {
|
| 3 |
assertBackendPolicy,
|
|
|
|
| 4 |
evaluateModelGate,
|
| 5 |
inspectWebGpuAdapter,
|
| 6 |
supportsWllamaWebGpuRuntime,
|
|
@@ -164,6 +165,52 @@ describe('evaluateModelGate', () => {
|
|
| 164 |
});
|
| 165 |
});
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
describe('assertBackendPolicy', () => {
|
| 168 |
const strictModel = model('27b', false);
|
| 169 |
const baseReport = {
|
|
|
|
| 1 |
import { afterEach, describe, expect, it, vi } from 'vitest';
|
| 2 |
import {
|
| 3 |
assertBackendPolicy,
|
| 4 |
+
evaluateModelContextPolicy,
|
| 5 |
evaluateModelGate,
|
| 6 |
inspectWebGpuAdapter,
|
| 7 |
supportsWllamaWebGpuRuntime,
|
|
|
|
| 165 |
});
|
| 166 |
});
|
| 167 |
|
| 168 |
+
describe('evaluateModelContextPolicy', () => {
|
| 169 |
+
const model27b = model('27b', false);
|
| 170 |
+
const longContextExperiment = {
|
| 171 |
+
tuningScope: 'benchmark',
|
| 172 |
+
requestedBackend: 'webgpu',
|
| 173 |
+
flashMode: 'auto',
|
| 174 |
+
kvCacheType: 'q4_0',
|
| 175 |
+
} as const;
|
| 176 |
+
|
| 177 |
+
it('allows the bounded 8,448-token 27B benchmark experiment', () => {
|
| 178 |
+
expect(evaluateModelContextPolicy(model27b, 8_448, longContextExperiment)).toEqual({
|
| 179 |
+
allowed: true,
|
| 180 |
+
limit: 8_448,
|
| 181 |
+
});
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
it('rejects 8,449 tokens even for the exact long-context experiment', () => {
|
| 185 |
+
expect(evaluateModelContextPolicy(model27b, 8_449, longContextExperiment)).toEqual({
|
| 186 |
+
allowed: false,
|
| 187 |
+
limit: 8_448,
|
| 188 |
+
});
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
it.each([
|
| 192 |
+
['q8 KV', { ...longContextExperiment, kvCacheType: 'q8_0' as const }],
|
| 193 |
+
['f16 KV', { ...longContextExperiment, kvCacheType: 'f16' as const }],
|
| 194 |
+
['auto backend', { ...longContextExperiment, requestedBackend: 'auto' as const }],
|
| 195 |
+
['release defaults', { ...longContextExperiment, tuningScope: 'release-defaults' as const }],
|
| 196 |
+
])('keeps 27B at 2,048 for %s', (_label, input) => {
|
| 197 |
+
expect(evaluateModelContextPolicy(model27b, 8_448, input)).toEqual({
|
| 198 |
+
allowed: false,
|
| 199 |
+
limit: 2_048,
|
| 200 |
+
});
|
| 201 |
+
expect(evaluateModelContextPolicy(model27b, 2_048, input).allowed).toBe(true);
|
| 202 |
+
});
|
| 203 |
+
|
| 204 |
+
it('leaves dense-tier manifest limits unchanged', () => {
|
| 205 |
+
expect(evaluateModelContextPolicy(model('8b', true), 32_768, {
|
| 206 |
+
tuningScope: 'release-defaults',
|
| 207 |
+
requestedBackend: 'auto',
|
| 208 |
+
flashMode: 'off',
|
| 209 |
+
kvCacheType: 'f16',
|
| 210 |
+
})).toEqual({ allowed: true, limit: 32_768 });
|
| 211 |
+
});
|
| 212 |
+
});
|
| 213 |
+
|
| 214 |
describe('assertBackendPolicy', () => {
|
| 215 |
const strictModel = model('27b', false);
|
| 216 |
const baseReport = {
|
src/engine/device-gate.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
import type { ManifestModelV2 } from './manifest';
|
| 2 |
import type {
|
|
|
|
|
|
|
| 3 |
GateDecision,
|
| 4 |
RequestedBackend,
|
| 5 |
RuntimeBackend,
|
|
@@ -8,6 +10,21 @@ import type {
|
|
| 8 |
} from './protocol';
|
| 9 |
import type { BackendReport } from './native-log';
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
const KNOWN_LIMITS = [
|
| 12 |
'maxBindGroups',
|
| 13 |
'maxBindingsPerBindGroup',
|
|
@@ -161,6 +178,30 @@ function availableStorageBytes(storage: StorageEstimate, cachedModelBytes: numbe
|
|
| 161 |
return Math.max(0, storage.quotaBytes - storage.usageBytes + cachedModelBytes);
|
| 162 |
}
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
function webGpuFailures(model: ManifestModelV2, adapter: WebGpuAdapterSnapshot): string[] {
|
| 165 |
if (!adapter.available) {
|
| 166 |
return ['WebGPU adapter is unavailable.'];
|
|
|
|
| 1 |
import type { ManifestModelV2 } from './manifest';
|
| 2 |
import type {
|
| 3 |
+
BenchmarkFlashMode,
|
| 4 |
+
BenchmarkKvCacheType,
|
| 5 |
GateDecision,
|
| 6 |
RequestedBackend,
|
| 7 |
RuntimeBackend,
|
|
|
|
| 10 |
} from './protocol';
|
| 11 |
import type { BackendReport } from './native-log';
|
| 12 |
|
| 13 |
+
export const MODEL_27B_RELEASE_CONTEXT_LIMIT = 2_048;
|
| 14 |
+
export const MODEL_27B_BENCHMARK_CONTEXT_LIMIT = 8_448;
|
| 15 |
+
|
| 16 |
+
export interface ModelContextPolicyInput {
|
| 17 |
+
tuningScope: 'release-defaults' | 'benchmark';
|
| 18 |
+
requestedBackend: RequestedBackend;
|
| 19 |
+
flashMode: BenchmarkFlashMode;
|
| 20 |
+
kvCacheType: BenchmarkKvCacheType;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface ModelContextPolicyDecision {
|
| 24 |
+
allowed: boolean;
|
| 25 |
+
limit: number;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
const KNOWN_LIMITS = [
|
| 29 |
'maxBindGroups',
|
| 30 |
'maxBindingsPerBindGroup',
|
|
|
|
| 178 |
return Math.max(0, storage.quotaBytes - storage.usageBytes + cachedModelBytes);
|
| 179 |
}
|
| 180 |
|
| 181 |
+
export function evaluateModelContextPolicy(
|
| 182 |
+
model: ManifestModelV2,
|
| 183 |
+
contextSize: number,
|
| 184 |
+
input: ModelContextPolicyInput,
|
| 185 |
+
): ModelContextPolicyDecision {
|
| 186 |
+
const longContextExperiment = model.id === '27b'
|
| 187 |
+
&& input.tuningScope === 'benchmark'
|
| 188 |
+
&& input.requestedBackend === 'webgpu'
|
| 189 |
+
&& input.flashMode === 'auto'
|
| 190 |
+
&& input.kvCacheType === 'q4_0';
|
| 191 |
+
const limit = model.id === '27b'
|
| 192 |
+
? Math.min(
|
| 193 |
+
model.contextLength,
|
| 194 |
+
longContextExperiment
|
| 195 |
+
? MODEL_27B_BENCHMARK_CONTEXT_LIMIT
|
| 196 |
+
: MODEL_27B_RELEASE_CONTEXT_LIMIT,
|
| 197 |
+
)
|
| 198 |
+
: model.contextLength;
|
| 199 |
+
return {
|
| 200 |
+
allowed: Number.isSafeInteger(contextSize) && contextSize > 0 && contextSize <= limit,
|
| 201 |
+
limit,
|
| 202 |
+
};
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
function webGpuFailures(model: ManifestModelV2, adapter: WebGpuAdapterSnapshot): string[] {
|
| 206 |
if (!adapter.available) {
|
| 207 |
return ['WebGPU adapter is unavailable.'];
|
src/engine/errors.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
export interface WebGpuDeviceLostDetails {
|
| 2 |
recoverable: true;
|
| 3 |
nextAction: 'reload-model';
|
| 4 |
-
stage: 'load' | 'generate' | 'backend-report';
|
| 5 |
modelId: string;
|
| 6 |
cpuFallbackAvailable: boolean;
|
| 7 |
signal: {
|
|
|
|
| 1 |
export interface WebGpuDeviceLostDetails {
|
| 2 |
recoverable: true;
|
| 3 |
nextAction: 'reload-model';
|
| 4 |
+
stage: 'load' | 'generate' | 'score-sequence' | 'backend-report';
|
| 5 |
modelId: string;
|
| 6 |
cpuFallbackAvailable: boolean;
|
| 7 |
signal: {
|
src/engine/protocol.ts
CHANGED
|
@@ -133,6 +133,7 @@ export interface GenerateParams {
|
|
| 133 |
tools?: EngineChatTool[];
|
| 134 |
toolChoice?: 'none' | 'auto' | 'required';
|
| 135 |
cachePrompt?: boolean;
|
|
|
|
| 136 |
}
|
| 137 |
|
| 138 |
export interface LoadModelResult {
|
|
@@ -180,8 +181,21 @@ export interface LoadModelResult {
|
|
| 180 |
backendReport: BackendReport;
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
export interface GenerateResult {
|
| 184 |
text: string;
|
|
|
|
|
|
|
|
|
|
| 185 |
finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null;
|
| 186 |
toolCalls: EngineChatToolCall[];
|
| 187 |
usage: {
|
|
@@ -195,6 +209,40 @@ export interface GenerateResult {
|
|
| 195 |
} | null;
|
| 196 |
}
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
export interface EmptyParams {
|
| 199 |
readonly _empty?: never;
|
| 200 |
}
|
|
@@ -203,6 +251,7 @@ export interface EngineRequestMap {
|
|
| 203 |
capabilities: EmptyParams;
|
| 204 |
loadModel: LoadModelParams;
|
| 205 |
generate: GenerateParams;
|
|
|
|
| 206 |
abort: { targetRequestId: string };
|
| 207 |
unload: EmptyParams;
|
| 208 |
backendReport: EmptyParams;
|
|
@@ -215,6 +264,7 @@ export interface EngineResponseMap {
|
|
| 215 |
capabilities: EngineCapabilities;
|
| 216 |
loadModel: LoadModelResult;
|
| 217 |
generate: GenerateResult;
|
|
|
|
| 218 |
abort: { targetRequestId: string; aborted: boolean };
|
| 219 |
unload: { unloaded: true };
|
| 220 |
backendReport: BackendReport;
|
|
|
|
| 133 |
tools?: EngineChatTool[];
|
| 134 |
toolChoice?: 'none' | 'auto' | 'required';
|
| 135 |
cachePrompt?: boolean;
|
| 136 |
+
returnTokenIds?: boolean;
|
| 137 |
}
|
| 138 |
|
| 139 |
export interface LoadModelResult {
|
|
|
|
| 181 |
backendReport: BackendReport;
|
| 182 |
}
|
| 183 |
|
| 184 |
+
export interface EngineTokenLogprob {
|
| 185 |
+
id: number;
|
| 186 |
+
logprob: number;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
export interface EngineSampledTokenTraceEntry {
|
| 190 |
+
selected: EngineTokenLogprob;
|
| 191 |
+
topCandidates: EngineTokenLogprob[];
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
export interface GenerateResult {
|
| 195 |
text: string;
|
| 196 |
+
reasoningText: string;
|
| 197 |
+
tokenIds: number[] | null;
|
| 198 |
+
tokenTrace: EngineSampledTokenTraceEntry[] | null;
|
| 199 |
finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null;
|
| 200 |
toolCalls: EngineChatToolCall[];
|
| 201 |
usage: {
|
|
|
|
| 209 |
} | null;
|
| 210 |
}
|
| 211 |
|
| 212 |
+
export interface ScoreSequenceParams {
|
| 213 |
+
promptTokenIds: number[];
|
| 214 |
+
referenceTokenIds: number[];
|
| 215 |
+
topK: 5;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
export interface EngineTeacherForcedScoreEntry {
|
| 219 |
+
index: number;
|
| 220 |
+
selectedReference: EngineTokenLogprob;
|
| 221 |
+
naturalTop1: EngineTokenLogprob;
|
| 222 |
+
topCandidates: EngineTokenLogprob[];
|
| 223 |
+
referenceRankInTopCandidatesZeroBased: number | null;
|
| 224 |
+
top1Top2Margin: number;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
export interface ScoreSequenceResult {
|
| 228 |
+
method: {
|
| 229 |
+
promptMode: 'raw-token-id-prefix';
|
| 230 |
+
maxTokensPerStep: 1;
|
| 231 |
+
temperature: 0;
|
| 232 |
+
topK: 1;
|
| 233 |
+
reportedTopLogprobs: 5;
|
| 234 |
+
logitBias: 1_000;
|
| 235 |
+
cachePromptFirst: false;
|
| 236 |
+
cachePromptSubsequent: true;
|
| 237 |
+
};
|
| 238 |
+
entries: EngineTeacherForcedScoreEntry[];
|
| 239 |
+
summary: {
|
| 240 |
+
tokenCount: 1_024;
|
| 241 |
+
meanNll: number;
|
| 242 |
+
perplexity: number;
|
| 243 |
+
};
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
export interface EmptyParams {
|
| 247 |
readonly _empty?: never;
|
| 248 |
}
|
|
|
|
| 251 |
capabilities: EmptyParams;
|
| 252 |
loadModel: LoadModelParams;
|
| 253 |
generate: GenerateParams;
|
| 254 |
+
scoreSequence: ScoreSequenceParams;
|
| 255 |
abort: { targetRequestId: string };
|
| 256 |
unload: EmptyParams;
|
| 257 |
backendReport: EmptyParams;
|
|
|
|
| 264 |
capabilities: EngineCapabilities;
|
| 265 |
loadModel: LoadModelResult;
|
| 266 |
generate: GenerateResult;
|
| 267 |
+
scoreSequence: ScoreSequenceResult;
|
| 268 |
abort: { targetRequestId: string; aborted: boolean };
|
| 269 |
unload: { unloaded: true };
|
| 270 |
backendReport: BackendReport;
|
src/engine/runtime-generate.test.ts
CHANGED
|
@@ -2,12 +2,16 @@ import { describe, expect, it } from 'vitest';
|
|
| 2 |
import type { ChatCompletionChunk } from '../../vendor/wllama-bonsai/esm/index.js';
|
| 3 |
import { BrowserEngineRuntime } from './runtime';
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
interface RuntimeInternals {
|
| 6 |
wllama: {
|
| 7 |
isModelLoaded(): boolean;
|
| 8 |
-
createChatCompletion(options:
|
| 9 |
-
onData(chunk: ChatCompletionChunk): void;
|
| 10 |
-
}): Promise<void>;
|
| 11 |
} | null;
|
| 12 |
loaded: {
|
| 13 |
manifest: unknown;
|
|
@@ -44,6 +48,46 @@ function chunk(
|
|
| 44 |
};
|
| 45 |
}
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
describe('BrowserEngineRuntime generation telemetry', () => {
|
| 48 |
it('retains usage and timings when a trailing stream chunk omits metadata', async () => {
|
| 49 |
const runtime = new BrowserEngineRuntime();
|
|
@@ -62,10 +106,12 @@ describe('BrowserEngineRuntime generation telemetry', () => {
|
|
| 62 |
},
|
| 63 |
},
|
| 64 |
};
|
|
|
|
| 65 |
internals.wllama = {
|
| 66 |
isModelLoaded: () => true,
|
| 67 |
-
createChatCompletion: async (
|
| 68 |
-
|
|
|
|
| 69 |
usage: { prompt_tokens: 9, completion_tokens: 1, total_tokens: 10 },
|
| 70 |
timings: {
|
| 71 |
cache_n: 0,
|
|
@@ -79,7 +125,7 @@ describe('BrowserEngineRuntime generation telemetry', () => {
|
|
| 79 |
predicted_per_second: 50,
|
| 80 |
},
|
| 81 |
}));
|
| 82 |
-
onData(chunk(''));
|
| 83 |
},
|
| 84 |
};
|
| 85 |
|
|
@@ -92,6 +138,11 @@ describe('BrowserEngineRuntime generation telemetry', () => {
|
|
| 92 |
|
| 93 |
expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 1, totalTokens: 10 });
|
| 94 |
expect(result.timings).toEqual({ promptTokensPerSecond: 100, predictedTokensPerSecond: 50 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
});
|
| 96 |
|
| 97 |
it('reconciles incomplete streamed usage with llama.cpp timing counts', async () => {
|
|
@@ -140,5 +191,310 @@ describe('BrowserEngineRuntime generation telemetry', () => {
|
|
| 140 |
);
|
| 141 |
|
| 142 |
expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 64, totalTokens: 73 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
});
|
| 144 |
});
|
|
|
|
| 2 |
import type { ChatCompletionChunk } from '../../vendor/wllama-bonsai/esm/index.js';
|
| 3 |
import { BrowserEngineRuntime } from './runtime';
|
| 4 |
|
| 5 |
+
interface CompletionOptions {
|
| 6 |
+
onData(chunk: ChatCompletionChunk): void;
|
| 7 |
+
logprobs?: boolean;
|
| 8 |
+
top_logprobs?: number;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
interface RuntimeInternals {
|
| 12 |
wllama: {
|
| 13 |
isModelLoaded(): boolean;
|
| 14 |
+
createChatCompletion(options: CompletionOptions): Promise<void>;
|
|
|
|
|
|
|
| 15 |
} | null;
|
| 16 |
loaded: {
|
| 17 |
manifest: unknown;
|
|
|
|
| 48 |
};
|
| 49 |
}
|
| 50 |
|
| 51 |
+
function tracedChunk(
|
| 52 |
+
content: string,
|
| 53 |
+
id: number,
|
| 54 |
+
metadata: Partial<Pick<ChatCompletionChunk, 'usage' | 'timings'>> = {},
|
| 55 |
+
): ChatCompletionChunk {
|
| 56 |
+
const value = chunk(content, metadata);
|
| 57 |
+
const choice = value.choices[0];
|
| 58 |
+
if (!choice) throw new Error('Chunk fixture requires one choice.');
|
| 59 |
+
choice.logprobs = {
|
| 60 |
+
content: [{
|
| 61 |
+
id,
|
| 62 |
+
token: content,
|
| 63 |
+
logprob: -0.1,
|
| 64 |
+
bytes: null,
|
| 65 |
+
top_logprobs: [
|
| 66 |
+
{ id: id + 1_004, token: 'fifth', logprob: -4, bytes: null },
|
| 67 |
+
{ id: id + 1_002, token: 'third', logprob: -2, bytes: null },
|
| 68 |
+
{ id, token: content, logprob: -0.1, bytes: null },
|
| 69 |
+
{ id: id + 1_003, token: 'fourth', logprob: -3, bytes: null },
|
| 70 |
+
{ id: id + 1_001, token: 'second', logprob: -1, bytes: null },
|
| 71 |
+
],
|
| 72 |
+
}],
|
| 73 |
+
refusal: null,
|
| 74 |
+
};
|
| 75 |
+
return value;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function reasoningTracedChunk(
|
| 79 |
+
reasoningContent: string,
|
| 80 |
+
id: number,
|
| 81 |
+
metadata: Partial<Pick<ChatCompletionChunk, 'usage' | 'timings'>> = {},
|
| 82 |
+
): ChatCompletionChunk {
|
| 83 |
+
const value = tracedChunk('', id, metadata);
|
| 84 |
+
const choice = value.choices[0];
|
| 85 |
+
if (!choice) throw new Error('Chunk fixture requires one choice.');
|
| 86 |
+
choice.finish_reason = null;
|
| 87 |
+
(choice.delta as unknown as Record<string, unknown>).reasoning_content = reasoningContent;
|
| 88 |
+
return value;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
describe('BrowserEngineRuntime generation telemetry', () => {
|
| 92 |
it('retains usage and timings when a trailing stream chunk omits metadata', async () => {
|
| 93 |
const runtime = new BrowserEngineRuntime();
|
|
|
|
| 106 |
},
|
| 107 |
},
|
| 108 |
};
|
| 109 |
+
let completionOptions: CompletionOptions | null = null;
|
| 110 |
internals.wllama = {
|
| 111 |
isModelLoaded: () => true,
|
| 112 |
+
createChatCompletion: async (options) => {
|
| 113 |
+
completionOptions = options;
|
| 114 |
+
options.onData(chunk('Ready', {
|
| 115 |
usage: { prompt_tokens: 9, completion_tokens: 1, total_tokens: 10 },
|
| 116 |
timings: {
|
| 117 |
cache_n: 0,
|
|
|
|
| 125 |
predicted_per_second: 50,
|
| 126 |
},
|
| 127 |
}));
|
| 128 |
+
options.onData(chunk(''));
|
| 129 |
},
|
| 130 |
};
|
| 131 |
|
|
|
|
| 138 |
|
| 139 |
expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 1, totalTokens: 10 });
|
| 140 |
expect(result.timings).toEqual({ promptTokensPerSecond: 100, predictedTokensPerSecond: 50 });
|
| 141 |
+
expect(result.reasoningText).toBe('');
|
| 142 |
+
expect(result.tokenIds).toBeNull();
|
| 143 |
+
expect(result.tokenTrace).toBeNull();
|
| 144 |
+
expect(completionOptions).not.toHaveProperty('logprobs');
|
| 145 |
+
expect(completionOptions).not.toHaveProperty('top_logprobs');
|
| 146 |
});
|
| 147 |
|
| 148 |
it('reconciles incomplete streamed usage with llama.cpp timing counts', async () => {
|
|
|
|
| 191 |
);
|
| 192 |
|
| 193 |
expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 64, totalTokens: 73 });
|
| 194 |
+
expect(result.tokenIds).toBeNull();
|
| 195 |
+
expect(result.tokenTrace).toBeNull();
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
+
it('returns sampled token ids with selected logprobs and sorted top-five candidates', async () => {
|
| 199 |
+
const runtime = new BrowserEngineRuntime();
|
| 200 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 201 |
+
let completionOptions: CompletionOptions | null = null;
|
| 202 |
+
internals.loaded = {
|
| 203 |
+
manifest: {},
|
| 204 |
+
backend: 'wasm',
|
| 205 |
+
model: {
|
| 206 |
+
id: '1_7b',
|
| 207 |
+
displayName: 'Fixture Bonsai',
|
| 208 |
+
cpuFallback: true,
|
| 209 |
+
runtimePolicy: {
|
| 210 |
+
flashAttention: false,
|
| 211 |
+
tokenEmbeddingOnWebGPU: true,
|
| 212 |
+
requireSingleWebGPUGraph: false,
|
| 213 |
+
},
|
| 214 |
+
},
|
| 215 |
+
};
|
| 216 |
+
internals.wllama = {
|
| 217 |
+
isModelLoaded: () => true,
|
| 218 |
+
createChatCompletion: async (options) => {
|
| 219 |
+
completionOptions = options;
|
| 220 |
+
options.onData(tracedChunk('one', 101));
|
| 221 |
+
options.onData(tracedChunk(' two', 202, {
|
| 222 |
+
usage: { prompt_tokens: 4, completion_tokens: 2, total_tokens: 6 },
|
| 223 |
+
timings: {
|
| 224 |
+
cache_n: 0,
|
| 225 |
+
prompt_n: 4,
|
| 226 |
+
prompt_ms: 40,
|
| 227 |
+
prompt_per_token_ms: 10,
|
| 228 |
+
prompt_per_second: 100,
|
| 229 |
+
predicted_n: 2,
|
| 230 |
+
predicted_ms: 40,
|
| 231 |
+
predicted_per_token_ms: 20,
|
| 232 |
+
predicted_per_second: 50,
|
| 233 |
+
},
|
| 234 |
+
}));
|
| 235 |
+
options.onData(chunk(''));
|
| 236 |
+
},
|
| 237 |
+
};
|
| 238 |
+
|
| 239 |
+
const result = await runtime.generate(
|
| 240 |
+
'request-token-ids',
|
| 241 |
+
{ messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
|
| 242 |
+
new AbortController().signal,
|
| 243 |
+
() => undefined,
|
| 244 |
+
);
|
| 245 |
+
|
| 246 |
+
expect(result.tokenIds).toEqual([101, 202]);
|
| 247 |
+
expect(result.tokenTrace).toEqual([
|
| 248 |
+
{
|
| 249 |
+
selected: { id: 101, logprob: -0.1 },
|
| 250 |
+
topCandidates: [
|
| 251 |
+
{ id: 101, logprob: -0.1 },
|
| 252 |
+
{ id: 1_102, logprob: -1 },
|
| 253 |
+
{ id: 1_103, logprob: -2 },
|
| 254 |
+
{ id: 1_104, logprob: -3 },
|
| 255 |
+
{ id: 1_105, logprob: -4 },
|
| 256 |
+
],
|
| 257 |
+
},
|
| 258 |
+
{
|
| 259 |
+
selected: { id: 202, logprob: -0.1 },
|
| 260 |
+
topCandidates: [
|
| 261 |
+
{ id: 202, logprob: -0.1 },
|
| 262 |
+
{ id: 1_203, logprob: -1 },
|
| 263 |
+
{ id: 1_204, logprob: -2 },
|
| 264 |
+
{ id: 1_205, logprob: -3 },
|
| 265 |
+
{ id: 1_206, logprob: -4 },
|
| 266 |
+
],
|
| 267 |
+
},
|
| 268 |
+
]);
|
| 269 |
+
expect(completionOptions).toHaveProperty('logprobs', true);
|
| 270 |
+
expect(completionOptions).toHaveProperty('top_logprobs', 5);
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
it('keeps reasoning-only output separate from visible text while preserving its token trace', async () => {
|
| 274 |
+
const runtime = new BrowserEngineRuntime();
|
| 275 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 276 |
+
internals.loaded = {
|
| 277 |
+
manifest: {},
|
| 278 |
+
backend: 'wasm',
|
| 279 |
+
model: {
|
| 280 |
+
id: '1_7b',
|
| 281 |
+
displayName: 'Fixture Bonsai',
|
| 282 |
+
cpuFallback: true,
|
| 283 |
+
runtimePolicy: {
|
| 284 |
+
flashAttention: false,
|
| 285 |
+
tokenEmbeddingOnWebGPU: true,
|
| 286 |
+
requireSingleWebGPUGraph: false,
|
| 287 |
+
},
|
| 288 |
+
},
|
| 289 |
+
};
|
| 290 |
+
internals.wllama = {
|
| 291 |
+
isModelLoaded: () => true,
|
| 292 |
+
createChatCompletion: async ({ onData }) => {
|
| 293 |
+
onData(reasoningTracedChunk('private reasoning', 303, {
|
| 294 |
+
usage: { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
|
| 295 |
+
}));
|
| 296 |
+
onData(chunk(''));
|
| 297 |
+
},
|
| 298 |
+
};
|
| 299 |
+
const streamed: Array<{ text: string; reasoningDelta?: string }> = [];
|
| 300 |
+
|
| 301 |
+
const result = await runtime.generate(
|
| 302 |
+
'request-reasoning-trace',
|
| 303 |
+
{ messages: [{ role: 'user', content: 'Think.' }], returnTokenIds: true },
|
| 304 |
+
new AbortController().signal,
|
| 305 |
+
(event) => {
|
| 306 |
+
if (event.event === 'token') streamed.push(event);
|
| 307 |
+
},
|
| 308 |
+
);
|
| 309 |
+
|
| 310 |
+
expect(result.text).toBe('');
|
| 311 |
+
expect(result.reasoningText).toBe('private reasoning');
|
| 312 |
+
expect(result.tokenIds).toEqual([303]);
|
| 313 |
+
expect(result.tokenTrace).toHaveLength(1);
|
| 314 |
+
expect(streamed).toEqual([{ type: 'event', requestId: 'request-reasoning-trace', event: 'token', text: '', reasoningDelta: 'private reasoning' }]);
|
| 315 |
+
});
|
| 316 |
+
|
| 317 |
+
it('fails loudly when a sampled token id is non-integer', async () => {
|
| 318 |
+
const runtime = new BrowserEngineRuntime();
|
| 319 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 320 |
+
internals.loaded = {
|
| 321 |
+
manifest: {},
|
| 322 |
+
backend: 'wasm',
|
| 323 |
+
model: {
|
| 324 |
+
id: '1_7b',
|
| 325 |
+
displayName: 'Fixture Bonsai',
|
| 326 |
+
cpuFallback: true,
|
| 327 |
+
runtimePolicy: {
|
| 328 |
+
flashAttention: false,
|
| 329 |
+
tokenEmbeddingOnWebGPU: true,
|
| 330 |
+
requireSingleWebGPUGraph: false,
|
| 331 |
+
},
|
| 332 |
+
},
|
| 333 |
+
};
|
| 334 |
+
internals.wllama = {
|
| 335 |
+
isModelLoaded: () => true,
|
| 336 |
+
createChatCompletion: async ({ onData }) => {
|
| 337 |
+
onData(tracedChunk('bad', 1.5));
|
| 338 |
+
},
|
| 339 |
+
};
|
| 340 |
+
|
| 341 |
+
await expect(runtime.generate(
|
| 342 |
+
'request-invalid-token-id',
|
| 343 |
+
{ messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
|
| 344 |
+
new AbortController().signal,
|
| 345 |
+
() => undefined,
|
| 346 |
+
)).rejects.toMatchObject({ code: 'INVALID_TOKEN_ID_TRACE' });
|
| 347 |
+
});
|
| 348 |
+
|
| 349 |
+
it('fails loudly when a sampled logprob entry omits its token id', async () => {
|
| 350 |
+
const runtime = new BrowserEngineRuntime();
|
| 351 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 352 |
+
internals.loaded = {
|
| 353 |
+
manifest: {},
|
| 354 |
+
backend: 'wasm',
|
| 355 |
+
model: {
|
| 356 |
+
id: '1_7b',
|
| 357 |
+
displayName: 'Fixture Bonsai',
|
| 358 |
+
cpuFallback: true,
|
| 359 |
+
runtimePolicy: {
|
| 360 |
+
flashAttention: false,
|
| 361 |
+
tokenEmbeddingOnWebGPU: true,
|
| 362 |
+
requireSingleWebGPUGraph: false,
|
| 363 |
+
},
|
| 364 |
+
},
|
| 365 |
+
};
|
| 366 |
+
internals.wllama = {
|
| 367 |
+
isModelLoaded: () => true,
|
| 368 |
+
createChatCompletion: async ({ onData }) => {
|
| 369 |
+
const malformed = tracedChunk('missing', 123);
|
| 370 |
+
const entry = malformed.choices[0]?.logprobs?.content?.[0];
|
| 371 |
+
if (!entry) throw new Error('Malformed trace fixture requires one logprob entry.');
|
| 372 |
+
Reflect.deleteProperty(entry, 'id');
|
| 373 |
+
onData(malformed);
|
| 374 |
+
},
|
| 375 |
+
};
|
| 376 |
+
|
| 377 |
+
await expect(runtime.generate(
|
| 378 |
+
'request-missing-token-id-field',
|
| 379 |
+
{ messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
|
| 380 |
+
new AbortController().signal,
|
| 381 |
+
() => undefined,
|
| 382 |
+
)).rejects.toMatchObject({
|
| 383 |
+
code: 'INVALID_TOKEN_ID_TRACE',
|
| 384 |
+
details: { index: 0, id: undefined },
|
| 385 |
+
});
|
| 386 |
+
});
|
| 387 |
+
|
| 388 |
+
it('fails loudly when completion usage outnumbers sampled token ids', async () => {
|
| 389 |
+
const runtime = new BrowserEngineRuntime();
|
| 390 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 391 |
+
internals.loaded = {
|
| 392 |
+
manifest: {},
|
| 393 |
+
backend: 'wasm',
|
| 394 |
+
model: {
|
| 395 |
+
id: '1_7b',
|
| 396 |
+
displayName: 'Fixture Bonsai',
|
| 397 |
+
cpuFallback: true,
|
| 398 |
+
runtimePolicy: {
|
| 399 |
+
flashAttention: false,
|
| 400 |
+
tokenEmbeddingOnWebGPU: true,
|
| 401 |
+
requireSingleWebGPUGraph: false,
|
| 402 |
+
},
|
| 403 |
+
},
|
| 404 |
+
};
|
| 405 |
+
internals.wllama = {
|
| 406 |
+
isModelLoaded: () => true,
|
| 407 |
+
createChatCompletion: async ({ onData }) => {
|
| 408 |
+
onData(chunk('missing', {
|
| 409 |
+
usage: { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
|
| 410 |
+
}));
|
| 411 |
+
},
|
| 412 |
+
};
|
| 413 |
+
|
| 414 |
+
await expect(runtime.generate(
|
| 415 |
+
'request-missing-token-id',
|
| 416 |
+
{ messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
|
| 417 |
+
new AbortController().signal,
|
| 418 |
+
() => undefined,
|
| 419 |
+
)).rejects.toMatchObject({
|
| 420 |
+
code: 'INCOMPLETE_TOKEN_ID_TRACE',
|
| 421 |
+
details: { expected: 1, observed: 0 },
|
| 422 |
+
});
|
| 423 |
+
});
|
| 424 |
+
|
| 425 |
+
it('fails loudly when a sampled token does not have exactly five top candidates', async () => {
|
| 426 |
+
const runtime = new BrowserEngineRuntime();
|
| 427 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 428 |
+
internals.loaded = {
|
| 429 |
+
manifest: {},
|
| 430 |
+
backend: 'wasm',
|
| 431 |
+
model: {
|
| 432 |
+
id: '1_7b',
|
| 433 |
+
displayName: 'Fixture Bonsai',
|
| 434 |
+
cpuFallback: true,
|
| 435 |
+
runtimePolicy: {
|
| 436 |
+
flashAttention: false,
|
| 437 |
+
tokenEmbeddingOnWebGPU: true,
|
| 438 |
+
requireSingleWebGPUGraph: false,
|
| 439 |
+
},
|
| 440 |
+
},
|
| 441 |
+
};
|
| 442 |
+
internals.wllama = {
|
| 443 |
+
isModelLoaded: () => true,
|
| 444 |
+
createChatCompletion: async ({ onData }) => {
|
| 445 |
+
const malformed = tracedChunk('short', 404);
|
| 446 |
+
malformed.choices[0]?.logprobs?.content?.[0]?.top_logprobs.pop();
|
| 447 |
+
onData(malformed);
|
| 448 |
+
},
|
| 449 |
+
};
|
| 450 |
+
|
| 451 |
+
await expect(runtime.generate(
|
| 452 |
+
'request-short-top-logprobs',
|
| 453 |
+
{ messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
|
| 454 |
+
new AbortController().signal,
|
| 455 |
+
() => undefined,
|
| 456 |
+
)).rejects.toMatchObject({
|
| 457 |
+
code: 'INVALID_TOKEN_LOGPROB_TRACE',
|
| 458 |
+
details: { index: 0, expected: 5, observed: 4 },
|
| 459 |
+
});
|
| 460 |
+
});
|
| 461 |
+
|
| 462 |
+
it('fails loudly when a top candidate has an invalid logprob', async () => {
|
| 463 |
+
const runtime = new BrowserEngineRuntime();
|
| 464 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 465 |
+
internals.loaded = {
|
| 466 |
+
manifest: {},
|
| 467 |
+
backend: 'wasm',
|
| 468 |
+
model: {
|
| 469 |
+
id: '1_7b',
|
| 470 |
+
displayName: 'Fixture Bonsai',
|
| 471 |
+
cpuFallback: true,
|
| 472 |
+
runtimePolicy: {
|
| 473 |
+
flashAttention: false,
|
| 474 |
+
tokenEmbeddingOnWebGPU: true,
|
| 475 |
+
requireSingleWebGPUGraph: false,
|
| 476 |
+
},
|
| 477 |
+
},
|
| 478 |
+
};
|
| 479 |
+
internals.wllama = {
|
| 480 |
+
isModelLoaded: () => true,
|
| 481 |
+
createChatCompletion: async ({ onData }) => {
|
| 482 |
+
const malformed = tracedChunk('invalid', 505);
|
| 483 |
+
const candidate = malformed.choices[0]?.logprobs?.content?.[0]?.top_logprobs[1];
|
| 484 |
+
if (!candidate) throw new Error('Malformed trace fixture requires a top candidate.');
|
| 485 |
+
candidate.logprob = Number.NaN;
|
| 486 |
+
onData(malformed);
|
| 487 |
+
},
|
| 488 |
+
};
|
| 489 |
+
|
| 490 |
+
await expect(runtime.generate(
|
| 491 |
+
'request-invalid-top-logprob',
|
| 492 |
+
{ messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
|
| 493 |
+
new AbortController().signal,
|
| 494 |
+
() => undefined,
|
| 495 |
+
)).rejects.toMatchObject({
|
| 496 |
+
code: 'INVALID_TOKEN_LOGPROB_TRACE',
|
| 497 |
+
details: { index: 0, field: 'topCandidates[1].logprob' },
|
| 498 |
+
});
|
| 499 |
});
|
| 500 |
});
|
src/engine/runtime-score-sequence.test.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it, vi } from 'vitest';
|
| 2 |
+
import type { BackendReport } from './native-log';
|
| 3 |
+
import { BrowserEngineRuntime } from './runtime';
|
| 4 |
+
|
| 5 |
+
interface RawCompletionOptions {
|
| 6 |
+
prompt: number[];
|
| 7 |
+
max_tokens: number;
|
| 8 |
+
temperature: number;
|
| 9 |
+
top_k: number;
|
| 10 |
+
logprobs: number;
|
| 11 |
+
logit_bias: Record<string, number>;
|
| 12 |
+
cache_prompt: boolean;
|
| 13 |
+
post_sampling_probs: boolean;
|
| 14 |
+
abortSignal: AbortSignal;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface RuntimeInternals {
|
| 18 |
+
wllama: {
|
| 19 |
+
isModelLoaded(): boolean;
|
| 20 |
+
createCompletion(options: RawCompletionOptions): Promise<unknown>;
|
| 21 |
+
} | null;
|
| 22 |
+
loaded: {
|
| 23 |
+
manifest: unknown;
|
| 24 |
+
backend: 'webgpu';
|
| 25 |
+
tuningScope: 'benchmark';
|
| 26 |
+
contextSize: number;
|
| 27 |
+
batchSize: number;
|
| 28 |
+
microBatchSize: number;
|
| 29 |
+
vocabularySize: number;
|
| 30 |
+
model: {
|
| 31 |
+
id: '27b';
|
| 32 |
+
displayName: string;
|
| 33 |
+
cpuFallback: false;
|
| 34 |
+
runtimePolicy: {
|
| 35 |
+
flashAttention: false;
|
| 36 |
+
tokenEmbeddingOnWebGPU: true;
|
| 37 |
+
requireSingleWebGPUGraph: true;
|
| 38 |
+
};
|
| 39 |
+
};
|
| 40 |
+
} | null;
|
| 41 |
+
nativeLog: {
|
| 42 |
+
report(): BackendReport;
|
| 43 |
+
};
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const backendReport: BackendReport = {
|
| 47 |
+
backends: ['WebGPU'],
|
| 48 |
+
nGraphSplits: 1,
|
| 49 |
+
opsOnCpu: 0,
|
| 50 |
+
layersGpu: { offloaded: 65, total: 65 },
|
| 51 |
+
flashAttention: false,
|
| 52 |
+
cacheTypeK: 'f16',
|
| 53 |
+
cacheTypeV: 'f16',
|
| 54 |
+
webgpuKvBufferBytes: 128 * 1024 ** 2,
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
function configuredRuntime(createCompletion: (options: RawCompletionOptions) => Promise<unknown>) {
|
| 58 |
+
const runtime = new BrowserEngineRuntime();
|
| 59 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 60 |
+
internals.loaded = {
|
| 61 |
+
manifest: {},
|
| 62 |
+
backend: 'webgpu',
|
| 63 |
+
tuningScope: 'benchmark',
|
| 64 |
+
contextSize: 2_048,
|
| 65 |
+
batchSize: 32,
|
| 66 |
+
microBatchSize: 16,
|
| 67 |
+
vocabularySize: 248_320,
|
| 68 |
+
model: {
|
| 69 |
+
id: '27b',
|
| 70 |
+
displayName: 'Fixture Bonsai 27B',
|
| 71 |
+
cpuFallback: false,
|
| 72 |
+
runtimePolicy: {
|
| 73 |
+
flashAttention: false,
|
| 74 |
+
tokenEmbeddingOnWebGPU: true,
|
| 75 |
+
requireSingleWebGPUGraph: true,
|
| 76 |
+
},
|
| 77 |
+
},
|
| 78 |
+
};
|
| 79 |
+
internals.wllama = { isModelLoaded: () => true, createCompletion };
|
| 80 |
+
vi.spyOn(internals.nativeLog, 'report').mockReturnValue(backendReport);
|
| 81 |
+
return runtime;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
describe('BrowserEngineRuntime teacher-forced scoring', () => {
|
| 85 |
+
it('scores the exact CPU sequence with raw token prefixes and keeps natural top-1 separate', async () => {
|
| 86 |
+
const calls: RawCompletionOptions[] = [];
|
| 87 |
+
const createCompletion = vi.fn(async (options: RawCompletionOptions) => {
|
| 88 |
+
calls.push(options);
|
| 89 |
+
const index = options.prompt.length - 38;
|
| 90 |
+
const referenceId = Number(Object.keys(options.logit_bias)[0]);
|
| 91 |
+
const naturalTop1Id = index === 29 ? referenceId + 10 : referenceId;
|
| 92 |
+
const candidates = index === 29
|
| 93 |
+
? [
|
| 94 |
+
{ id: naturalTop1Id, token: 'natural', logprob: -0.01, bytes: null },
|
| 95 |
+
{ id: referenceId, token: 'reference', logprob: -0.02, bytes: null },
|
| 96 |
+
{ id: referenceId + 20, token: 'third', logprob: -1, bytes: null },
|
| 97 |
+
{ id: referenceId + 21, token: 'fourth', logprob: -2, bytes: null },
|
| 98 |
+
{ id: referenceId + 22, token: 'fifth', logprob: -3, bytes: null },
|
| 99 |
+
]
|
| 100 |
+
: [
|
| 101 |
+
{ id: referenceId, token: 'reference', logprob: -0.01, bytes: null },
|
| 102 |
+
{ id: referenceId + 10, token: 'second', logprob: -0.02, bytes: null },
|
| 103 |
+
{ id: referenceId + 20, token: 'third', logprob: -1, bytes: null },
|
| 104 |
+
{ id: referenceId + 21, token: 'fourth', logprob: -2, bytes: null },
|
| 105 |
+
{ id: referenceId + 22, token: 'fifth', logprob: -3, bytes: null },
|
| 106 |
+
];
|
| 107 |
+
return {
|
| 108 |
+
choices: [{
|
| 109 |
+
text: 'forced',
|
| 110 |
+
finish_reason: 'length',
|
| 111 |
+
logprobs: {
|
| 112 |
+
content: [{
|
| 113 |
+
id: referenceId,
|
| 114 |
+
token: 'reference',
|
| 115 |
+
logprob: index === 29 ? -0.02 : -0.01,
|
| 116 |
+
bytes: null,
|
| 117 |
+
top_logprobs: candidates,
|
| 118 |
+
}],
|
| 119 |
+
},
|
| 120 |
+
}],
|
| 121 |
+
};
|
| 122 |
+
});
|
| 123 |
+
const runtime = configuredRuntime(createCompletion);
|
| 124 |
+
const promptTokenIds = Array.from({ length: 38 }, (_, index) => index + 1);
|
| 125 |
+
const referenceTokenIds = Array.from({ length: 1_024 }, (_, index) => index + 1_000);
|
| 126 |
+
|
| 127 |
+
const result = await runtime.scoreSequence({
|
| 128 |
+
promptTokenIds,
|
| 129 |
+
referenceTokenIds,
|
| 130 |
+
topK: 5,
|
| 131 |
+
}, new AbortController().signal);
|
| 132 |
+
|
| 133 |
+
expect(createCompletion).toHaveBeenCalledTimes(1_024);
|
| 134 |
+
expect(calls[0]).toMatchObject({
|
| 135 |
+
prompt: promptTokenIds,
|
| 136 |
+
max_tokens: 1,
|
| 137 |
+
temperature: 0,
|
| 138 |
+
top_k: 1,
|
| 139 |
+
logprobs: 5,
|
| 140 |
+
logit_bias: { '1000': 1_000 },
|
| 141 |
+
cache_prompt: false,
|
| 142 |
+
post_sampling_probs: false,
|
| 143 |
+
});
|
| 144 |
+
expect(calls[1]).toMatchObject({
|
| 145 |
+
prompt: [...promptTokenIds, 1_000],
|
| 146 |
+
cache_prompt: true,
|
| 147 |
+
});
|
| 148 |
+
expect(calls.at(-1)?.prompt).toEqual([
|
| 149 |
+
...promptTokenIds,
|
| 150 |
+
...referenceTokenIds.slice(0, -1),
|
| 151 |
+
]);
|
| 152 |
+
expect(result.entries[29]).toMatchObject({
|
| 153 |
+
index: 29,
|
| 154 |
+
selectedReference: { id: 1_029, logprob: -0.02 },
|
| 155 |
+
naturalTop1: { id: 1_039, logprob: -0.01 },
|
| 156 |
+
referenceRankInTopCandidatesZeroBased: 1,
|
| 157 |
+
top1Top2Margin: 0.01,
|
| 158 |
+
});
|
| 159 |
+
expect(result.summary.tokenCount).toBe(1_024);
|
| 160 |
+
expect(result.summary.meanNll).toBeCloseTo((1_023 * 0.01 + 0.02) / 1_024, 12);
|
| 161 |
+
expect(result.summary.perplexity).toBeCloseTo(Math.exp(result.summary.meanNll), 12);
|
| 162 |
+
});
|
| 163 |
+
|
| 164 |
+
it('honors an already-aborted diagnostic request before the first raw completion', async () => {
|
| 165 |
+
const createCompletion = vi.fn(async () => ({}));
|
| 166 |
+
const runtime = configuredRuntime(createCompletion);
|
| 167 |
+
const controller = new AbortController();
|
| 168 |
+
controller.abort();
|
| 169 |
+
|
| 170 |
+
await expect(runtime.scoreSequence({
|
| 171 |
+
promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
|
| 172 |
+
referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
|
| 173 |
+
topK: 5,
|
| 174 |
+
}, controller.signal)).rejects.toMatchObject({ name: 'AbortError' });
|
| 175 |
+
expect(createCompletion).not.toHaveBeenCalled();
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
it('fails loudly when logit bias does not return the fixed reference token', async () => {
|
| 179 |
+
const runtime = configuredRuntime(async (options) => {
|
| 180 |
+
const referenceId = Number(Object.keys(options.logit_bias)[0]);
|
| 181 |
+
const selectedId = referenceId + 1;
|
| 182 |
+
return {
|
| 183 |
+
choices: [{
|
| 184 |
+
logprobs: {
|
| 185 |
+
content: [{
|
| 186 |
+
id: selectedId,
|
| 187 |
+
logprob: -0.01,
|
| 188 |
+
top_logprobs: [
|
| 189 |
+
{ id: selectedId, logprob: -0.01 },
|
| 190 |
+
{ id: referenceId, logprob: -0.02 },
|
| 191 |
+
{ id: referenceId + 2, logprob: -1 },
|
| 192 |
+
{ id: referenceId + 3, logprob: -2 },
|
| 193 |
+
{ id: referenceId + 4, logprob: -3 },
|
| 194 |
+
],
|
| 195 |
+
}],
|
| 196 |
+
},
|
| 197 |
+
}],
|
| 198 |
+
};
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
await expect(runtime.scoreSequence({
|
| 202 |
+
promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
|
| 203 |
+
referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
|
| 204 |
+
topK: 5,
|
| 205 |
+
}, new AbortController().signal)).rejects.toMatchObject({
|
| 206 |
+
code: 'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 207 |
+
details: { index: 0, referenceTokenId: 1_000 },
|
| 208 |
+
});
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
it('rejects scoring outside the loaded 27B WebGPU benchmark path', async () => {
|
| 212 |
+
const runtime = configuredRuntime(async () => ({}));
|
| 213 |
+
const internals = runtime as unknown as RuntimeInternals;
|
| 214 |
+
if (!internals.loaded) throw new Error('Expected loaded fixture state.');
|
| 215 |
+
(internals.loaded as { tuningScope: string }).tuningScope = 'release-defaults';
|
| 216 |
+
|
| 217 |
+
await expect(runtime.scoreSequence({
|
| 218 |
+
promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
|
| 219 |
+
referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
|
| 220 |
+
topK: 5,
|
| 221 |
+
}, new AbortController().signal)).rejects.toMatchObject({
|
| 222 |
+
code: 'SCORE_SEQUENCE_UNAVAILABLE',
|
| 223 |
+
});
|
| 224 |
+
});
|
| 225 |
+
});
|
src/engine/runtime.ts
CHANGED
|
@@ -10,6 +10,7 @@ import runtimeSource from '../../vendor/wllama-bonsai/SOURCE.json';
|
|
| 10 |
import {
|
| 11 |
assertBackendPolicy,
|
| 12 |
estimateStorage,
|
|
|
|
| 13 |
evaluateModelGate,
|
| 14 |
inspectWebGpuAdapter,
|
| 15 |
persistStorage,
|
|
@@ -36,11 +37,14 @@ import type {
|
|
| 36 |
BenchmarkWasmFlavor,
|
| 37 |
EngineCapabilities,
|
| 38 |
EngineEvent,
|
|
|
|
| 39 |
GenerateParams,
|
| 40 |
GenerateResult,
|
| 41 |
LoadModelParams,
|
| 42 |
LoadModelResult,
|
| 43 |
RuntimeBackend,
|
|
|
|
|
|
|
| 44 |
ShardDownloadFailureDetails,
|
| 45 |
StorageEstimate,
|
| 46 |
} from './protocol';
|
|
@@ -52,6 +56,10 @@ const WLLAMA_COMPAT_WASM_PATH = '/wasm/wllama-compat.wasm';
|
|
| 52 |
const WLLAMA_COMPAT_WORKER_PATH = '/wasm/wllama-compat.js';
|
| 53 |
const MAX_GLUE_INT = 2_147_483_647;
|
| 54 |
const DEVICE_LOST_EXIT_GRACE_MS = 100;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
export interface ResolvedLoadTuning {
|
| 57 |
scope: 'release-defaults' | 'benchmark';
|
|
@@ -183,6 +191,11 @@ interface LoadedModelState {
|
|
| 183 |
manifest: ModelManifestV2;
|
| 184 |
model: ManifestModelV2;
|
| 185 |
backend: RuntimeBackend;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
interface CachedShardState {
|
|
@@ -312,6 +325,244 @@ function mapTimings(timings: ResultTimings | undefined): GenerateResult['timings
|
|
| 312 |
};
|
| 313 |
}
|
| 314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
function asReasoningDelta(chunk: ChatCompletionChunk): string | undefined {
|
| 316 |
const choice = chunk.choices[0] as unknown as Record<string, unknown> | undefined;
|
| 317 |
const delta = choice?.delta;
|
|
@@ -685,6 +936,19 @@ export class BrowserEngineRuntime {
|
|
| 685 |
shardPath: null,
|
| 686 |
});
|
| 687 |
const model = findManifestModel(manifest, params.modelId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
const urls = orderedShardUrls(manifest, model);
|
| 689 |
const wllama = await this.ensureWllama(wasmFlavor);
|
| 690 |
const cached = await this.inspectCachedShards(wllama, model, urls, signal);
|
|
@@ -713,13 +977,6 @@ export class BrowserEngineRuntime {
|
|
| 713 |
shardCount: model.files.length,
|
| 714 |
shardPath: null,
|
| 715 |
});
|
| 716 |
-
const contextSize = params.contextSize ?? model.defaultContext;
|
| 717 |
-
if (!Number.isSafeInteger(contextSize) || contextSize <= 0 || contextSize > model.contextLength) {
|
| 718 |
-
throw new EngineRuntimeError(
|
| 719 |
-
'INVALID_CONTEXT_SIZE',
|
| 720 |
-
`Context size must be between 1 and ${model.contextLength}.`,
|
| 721 |
-
);
|
| 722 |
-
}
|
| 723 |
const defaultThreads = Math.max(1, Math.floor((navigator.hardwareConcurrency || 1) / 2));
|
| 724 |
const threads = params.threads ?? (gate.selectedBackend === 'wasm' ? defaultThreads : 1);
|
| 725 |
if (!Number.isSafeInteger(threads) || threads <= 0) {
|
|
@@ -801,7 +1058,16 @@ export class BrowserEngineRuntime {
|
|
| 801 |
tuningResult,
|
| 802 |
);
|
| 803 |
}
|
| 804 |
-
this.loaded = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
emitProgress(sink, requestId, {
|
| 806 |
phase: 'load',
|
| 807 |
loadedBytes: model.downloadBytes,
|
|
@@ -860,9 +1126,11 @@ export class BrowserEngineRuntime {
|
|
| 860 |
}
|
| 861 |
throwIfAborted(signal);
|
| 862 |
let text = '';
|
|
|
|
| 863 |
let finishReason: GenerateResult['finishReason'] = null;
|
| 864 |
let usage: GenerateResult['usage'] = null;
|
| 865 |
let timings: GenerateResult['timings'] = null;
|
|
|
|
| 866 |
const streamedToolCalls = new ToolCallAccumulator();
|
| 867 |
try {
|
| 868 |
await this.raceWebGpuDeviceLoss('generate', loaded.backend, loaded.model, wllama.createChatCompletion({
|
|
@@ -877,15 +1145,20 @@ export class BrowserEngineRuntime {
|
|
| 877 |
tools: params.tools as ChatCompletionTool[] | undefined,
|
| 878 |
tool_choice: params.toolChoice,
|
| 879 |
cache_prompt: params.cachePrompt ?? true,
|
|
|
|
|
|
|
|
|
|
| 880 |
abortSignal: signal,
|
| 881 |
timings_per_token: true,
|
| 882 |
onData: (chunk: ChatCompletionChunk) => {
|
|
|
|
| 883 |
streamedToolCalls.append(chunk.choices[0]?.delta.tool_calls);
|
| 884 |
const delta = chunk.choices[0]?.delta.content;
|
| 885 |
const textDelta = typeof delta === 'string' ? delta : '';
|
| 886 |
const reasoningDelta = asReasoningDelta(chunk);
|
| 887 |
if (textDelta || reasoningDelta) {
|
| 888 |
text += textDelta;
|
|
|
|
| 889 |
sink({
|
| 890 |
type: 'event',
|
| 891 |
requestId,
|
|
@@ -906,6 +1179,8 @@ export class BrowserEngineRuntime {
|
|
| 906 |
}
|
| 907 |
await this.assertWebGpuAlive('generate', loaded.backend, loaded.model);
|
| 908 |
throwIfAborted(signal);
|
|
|
|
|
|
|
| 909 |
const report = this.nativeLog.report();
|
| 910 |
try {
|
| 911 |
assertBackendPolicy(loaded.model, loaded.backend, report);
|
|
@@ -926,7 +1201,172 @@ export class BrowserEngineRuntime {
|
|
| 926 |
error instanceof Error ? error.message : String(error),
|
| 927 |
);
|
| 928 |
}
|
| 929 |
-
return { text, finishReason, toolCalls, usage, timings };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
}
|
| 931 |
|
| 932 |
async backendReport(): Promise<BackendReport> {
|
|
|
|
| 10 |
import {
|
| 11 |
assertBackendPolicy,
|
| 12 |
estimateStorage,
|
| 13 |
+
evaluateModelContextPolicy,
|
| 14 |
evaluateModelGate,
|
| 15 |
inspectWebGpuAdapter,
|
| 16 |
persistStorage,
|
|
|
|
| 37 |
BenchmarkWasmFlavor,
|
| 38 |
EngineCapabilities,
|
| 39 |
EngineEvent,
|
| 40 |
+
EngineSampledTokenTraceEntry,
|
| 41 |
GenerateParams,
|
| 42 |
GenerateResult,
|
| 43 |
LoadModelParams,
|
| 44 |
LoadModelResult,
|
| 45 |
RuntimeBackend,
|
| 46 |
+
ScoreSequenceParams,
|
| 47 |
+
ScoreSequenceResult,
|
| 48 |
ShardDownloadFailureDetails,
|
| 49 |
StorageEstimate,
|
| 50 |
} from './protocol';
|
|
|
|
| 56 |
const WLLAMA_COMPAT_WORKER_PATH = '/wasm/wllama-compat.js';
|
| 57 |
const MAX_GLUE_INT = 2_147_483_647;
|
| 58 |
const DEVICE_LOST_EXIT_GRACE_MS = 100;
|
| 59 |
+
const TOKEN_TRACE_TOP_LOGPROBS = 5;
|
| 60 |
+
const STATE_DRIFT_REFERENCE_TOKENS = 1_024;
|
| 61 |
+
const STATE_DRIFT_PROMPT_TOKENS = 38;
|
| 62 |
+
const TEACHER_FORCE_LOGIT_BIAS = 1_000;
|
| 63 |
|
| 64 |
export interface ResolvedLoadTuning {
|
| 65 |
scope: 'release-defaults' | 'benchmark';
|
|
|
|
| 191 |
manifest: ModelManifestV2;
|
| 192 |
model: ManifestModelV2;
|
| 193 |
backend: RuntimeBackend;
|
| 194 |
+
tuningScope: ResolvedLoadTuning['scope'];
|
| 195 |
+
contextSize: number;
|
| 196 |
+
batchSize: number;
|
| 197 |
+
microBatchSize: number;
|
| 198 |
+
vocabularySize: number;
|
| 199 |
}
|
| 200 |
|
| 201 |
interface CachedShardState {
|
|
|
|
| 325 |
};
|
| 326 |
}
|
| 327 |
|
| 328 |
+
function traceTokenId(value: unknown, index: number, field: string): number {
|
| 329 |
+
if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > 0xffff_ffff) {
|
| 330 |
+
throw new EngineRuntimeError(
|
| 331 |
+
'INVALID_TOKEN_ID_TRACE',
|
| 332 |
+
'The native runtime returned a missing or invalid token id in the sampled-token trace.',
|
| 333 |
+
{ index, field, id: value },
|
| 334 |
+
);
|
| 335 |
+
}
|
| 336 |
+
return value as number;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
function traceLogprob(value: unknown, index: number, field: string): number {
|
| 340 |
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
| 341 |
+
throw new EngineRuntimeError(
|
| 342 |
+
'INVALID_TOKEN_LOGPROB_TRACE',
|
| 343 |
+
'The native runtime returned a missing or invalid logprob in the sampled-token trace.',
|
| 344 |
+
{ index, field, logprob: value },
|
| 345 |
+
);
|
| 346 |
+
}
|
| 347 |
+
return value;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function appendSampledTokenTrace(
|
| 351 |
+
target: EngineSampledTokenTraceEntry[],
|
| 352 |
+
chunk: ChatCompletionChunk,
|
| 353 |
+
): void {
|
| 354 |
+
const entries = chunk.choices[0]?.logprobs?.content;
|
| 355 |
+
if (!entries) return;
|
| 356 |
+
for (const entry of entries) {
|
| 357 |
+
const index = target.length;
|
| 358 |
+
const selected = {
|
| 359 |
+
id: traceTokenId(entry.id, index, 'selected.id'),
|
| 360 |
+
logprob: traceLogprob(entry.logprob, index, 'selected.logprob'),
|
| 361 |
+
};
|
| 362 |
+
if (!Array.isArray(entry.top_logprobs) || entry.top_logprobs.length !== TOKEN_TRACE_TOP_LOGPROBS) {
|
| 363 |
+
throw new EngineRuntimeError(
|
| 364 |
+
'INVALID_TOKEN_LOGPROB_TRACE',
|
| 365 |
+
`The native runtime must return exactly ${TOKEN_TRACE_TOP_LOGPROBS} top logprob candidates per sampled token.`,
|
| 366 |
+
{
|
| 367 |
+
index,
|
| 368 |
+
expected: TOKEN_TRACE_TOP_LOGPROBS,
|
| 369 |
+
observed: Array.isArray(entry.top_logprobs) ? entry.top_logprobs.length : null,
|
| 370 |
+
},
|
| 371 |
+
);
|
| 372 |
+
}
|
| 373 |
+
const seenIds = new Set<number>();
|
| 374 |
+
const topCandidates = entry.top_logprobs.map((candidate, candidateIndex) => {
|
| 375 |
+
const id = traceTokenId(candidate.id, index, `topCandidates[${candidateIndex}].id`);
|
| 376 |
+
if (seenIds.has(id)) {
|
| 377 |
+
throw new EngineRuntimeError(
|
| 378 |
+
'INVALID_TOKEN_LOGPROB_TRACE',
|
| 379 |
+
'The native runtime returned duplicate ids in a top-logprob candidate list.',
|
| 380 |
+
{ index, candidateIndex, id },
|
| 381 |
+
);
|
| 382 |
+
}
|
| 383 |
+
seenIds.add(id);
|
| 384 |
+
return {
|
| 385 |
+
id,
|
| 386 |
+
logprob: traceLogprob(
|
| 387 |
+
candidate.logprob,
|
| 388 |
+
index,
|
| 389 |
+
`topCandidates[${candidateIndex}].logprob`,
|
| 390 |
+
),
|
| 391 |
+
};
|
| 392 |
+
}).sort((left, right) => right.logprob - left.logprob || left.id - right.id);
|
| 393 |
+
const selectedCandidate = topCandidates.find((candidate) => candidate.id === selected.id);
|
| 394 |
+
if (!selectedCandidate || selectedCandidate.logprob !== selected.logprob) {
|
| 395 |
+
throw new EngineRuntimeError(
|
| 396 |
+
'INVALID_TOKEN_LOGPROB_TRACE',
|
| 397 |
+
'The sampled token must appear exactly once in its top-logprob candidates with the same logprob.',
|
| 398 |
+
{ index, selected, selectedCandidate: selectedCandidate ?? null },
|
| 399 |
+
);
|
| 400 |
+
}
|
| 401 |
+
target.push({ selected, topCandidates });
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
function assertCompleteTokenTrace(
|
| 406 |
+
tokenTrace: EngineSampledTokenTraceEntry[] | null,
|
| 407 |
+
usage: GenerateResult['usage'],
|
| 408 |
+
): void {
|
| 409 |
+
if (tokenTrace === null) return;
|
| 410 |
+
if (usage === null || tokenTrace.length !== usage.completionTokens) {
|
| 411 |
+
throw new EngineRuntimeError(
|
| 412 |
+
'INCOMPLETE_TOKEN_ID_TRACE',
|
| 413 |
+
'The native runtime did not return exactly one complete sampled-token trace per completion token.',
|
| 414 |
+
{
|
| 415 |
+
expected: usage?.completionTokens ?? null,
|
| 416 |
+
observed: tokenTrace.length,
|
| 417 |
+
},
|
| 418 |
+
);
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
function scoreRecord(value: unknown, index: number, field: string): Record<string, unknown> {
|
| 423 |
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
| 424 |
+
throw new EngineRuntimeError(
|
| 425 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 426 |
+
`Teacher-forced response ${index} has an invalid ${field}.`,
|
| 427 |
+
{ index, field },
|
| 428 |
+
);
|
| 429 |
+
}
|
| 430 |
+
return value as Record<string, unknown>;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
function scoreTokenId(
|
| 434 |
+
value: unknown,
|
| 435 |
+
index: number,
|
| 436 |
+
field: string,
|
| 437 |
+
vocabularySize: number,
|
| 438 |
+
): number {
|
| 439 |
+
if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) >= vocabularySize) {
|
| 440 |
+
throw new EngineRuntimeError(
|
| 441 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 442 |
+
`Teacher-forced response ${index} has an invalid ${field}.`,
|
| 443 |
+
{ index, field, id: value, vocabularySize },
|
| 444 |
+
);
|
| 445 |
+
}
|
| 446 |
+
return value as number;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function scoreLogprob(value: unknown, index: number, field: string): number {
|
| 450 |
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
| 451 |
+
throw new EngineRuntimeError(
|
| 452 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 453 |
+
`Teacher-forced response ${index} has an invalid ${field}.`,
|
| 454 |
+
{ index, field, logprob: value },
|
| 455 |
+
);
|
| 456 |
+
}
|
| 457 |
+
return value;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
function parseTeacherForcedResponse(
|
| 461 |
+
response: unknown,
|
| 462 |
+
index: number,
|
| 463 |
+
referenceTokenId: number,
|
| 464 |
+
vocabularySize: number,
|
| 465 |
+
): ScoreSequenceResult['entries'][number] {
|
| 466 |
+
const root = scoreRecord(response, index, 'root');
|
| 467 |
+
if (!Array.isArray(root.choices) || root.choices.length !== 1) {
|
| 468 |
+
throw new EngineRuntimeError(
|
| 469 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 470 |
+
`Teacher-forced response ${index} must contain exactly one completion choice.`,
|
| 471 |
+
{ index, observed: Array.isArray(root.choices) ? root.choices.length : null },
|
| 472 |
+
);
|
| 473 |
+
}
|
| 474 |
+
const choice = scoreRecord(root.choices[0], index, 'choices[0]');
|
| 475 |
+
const logprobs = scoreRecord(choice.logprobs, index, 'choices[0].logprobs');
|
| 476 |
+
if (!Array.isArray(logprobs.content) || logprobs.content.length !== 1) {
|
| 477 |
+
throw new EngineRuntimeError(
|
| 478 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 479 |
+
`Teacher-forced response ${index} must contain one selected-token logprob entry.`,
|
| 480 |
+
{ index, observed: Array.isArray(logprobs.content) ? logprobs.content.length : null },
|
| 481 |
+
);
|
| 482 |
+
}
|
| 483 |
+
const selectedEntry = scoreRecord(logprobs.content[0], index, 'logprobs.content[0]');
|
| 484 |
+
const selectedReference = {
|
| 485 |
+
id: scoreTokenId(selectedEntry.id, index, 'selectedReference.id', vocabularySize),
|
| 486 |
+
logprob: scoreLogprob(selectedEntry.logprob, index, 'selectedReference.logprob'),
|
| 487 |
+
};
|
| 488 |
+
if (selectedReference.id !== referenceTokenId) {
|
| 489 |
+
throw new EngineRuntimeError(
|
| 490 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 491 |
+
`Teacher forcing selected token ${selectedReference.id} instead of reference token ${referenceTokenId} at position ${index + 1}.`,
|
| 492 |
+
{ index, selectedTokenId: selectedReference.id, referenceTokenId },
|
| 493 |
+
);
|
| 494 |
+
}
|
| 495 |
+
const rawTopLogprobs = selectedEntry.top_logprobs;
|
| 496 |
+
if (!Array.isArray(rawTopLogprobs)
|
| 497 |
+
|| rawTopLogprobs.length !== TOKEN_TRACE_TOP_LOGPROBS) {
|
| 498 |
+
throw new EngineRuntimeError(
|
| 499 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 500 |
+
`Teacher-forced response ${index} must contain exactly ${TOKEN_TRACE_TOP_LOGPROBS} natural top candidates.`,
|
| 501 |
+
{
|
| 502 |
+
index,
|
| 503 |
+
observed: Array.isArray(rawTopLogprobs)
|
| 504 |
+
? rawTopLogprobs.length
|
| 505 |
+
: null,
|
| 506 |
+
},
|
| 507 |
+
);
|
| 508 |
+
}
|
| 509 |
+
const seenIds = new Set<number>();
|
| 510 |
+
const topCandidates = rawTopLogprobs.map((rawCandidate, candidateIndex) => {
|
| 511 |
+
const candidate = scoreRecord(
|
| 512 |
+
rawCandidate,
|
| 513 |
+
index,
|
| 514 |
+
`topCandidates[${candidateIndex}]`,
|
| 515 |
+
);
|
| 516 |
+
const parsed = {
|
| 517 |
+
id: scoreTokenId(
|
| 518 |
+
candidate.id,
|
| 519 |
+
index,
|
| 520 |
+
`topCandidates[${candidateIndex}].id`,
|
| 521 |
+
vocabularySize,
|
| 522 |
+
),
|
| 523 |
+
logprob: scoreLogprob(
|
| 524 |
+
candidate.logprob,
|
| 525 |
+
index,
|
| 526 |
+
`topCandidates[${candidateIndex}].logprob`,
|
| 527 |
+
),
|
| 528 |
+
};
|
| 529 |
+
if (seenIds.has(parsed.id)) {
|
| 530 |
+
throw new EngineRuntimeError(
|
| 531 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 532 |
+
`Teacher-forced response ${index} contains duplicate natural candidate id ${parsed.id}.`,
|
| 533 |
+
{ index, candidateIndex, id: parsed.id },
|
| 534 |
+
);
|
| 535 |
+
}
|
| 536 |
+
seenIds.add(parsed.id);
|
| 537 |
+
return parsed;
|
| 538 |
+
}).sort((left, right) => right.logprob - left.logprob || left.id - right.id);
|
| 539 |
+
const referenceRankInTopCandidatesZeroBased = topCandidates.findIndex(
|
| 540 |
+
(candidate) => candidate.id === referenceTokenId,
|
| 541 |
+
);
|
| 542 |
+
if (
|
| 543 |
+
referenceRankInTopCandidatesZeroBased !== -1
|
| 544 |
+
&& topCandidates[referenceRankInTopCandidatesZeroBased]?.logprob !== selectedReference.logprob
|
| 545 |
+
) {
|
| 546 |
+
throw new EngineRuntimeError(
|
| 547 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 548 |
+
`Teacher-forced response ${index} reports inconsistent reference logprobs.`,
|
| 549 |
+
{ index, selectedReference, referenceRankInTopCandidatesZeroBased },
|
| 550 |
+
);
|
| 551 |
+
}
|
| 552 |
+
const naturalTop1 = topCandidates[0]!;
|
| 553 |
+
const runnerUp = topCandidates[1]!;
|
| 554 |
+
return {
|
| 555 |
+
index,
|
| 556 |
+
selectedReference,
|
| 557 |
+
naturalTop1: { ...naturalTop1 },
|
| 558 |
+
topCandidates,
|
| 559 |
+
referenceRankInTopCandidatesZeroBased: referenceRankInTopCandidatesZeroBased === -1
|
| 560 |
+
? null
|
| 561 |
+
: referenceRankInTopCandidatesZeroBased,
|
| 562 |
+
top1Top2Margin: naturalTop1.logprob - runnerUp.logprob,
|
| 563 |
+
};
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
function asReasoningDelta(chunk: ChatCompletionChunk): string | undefined {
|
| 567 |
const choice = chunk.choices[0] as unknown as Record<string, unknown> | undefined;
|
| 568 |
const delta = choice?.delta;
|
|
|
|
| 936 |
shardPath: null,
|
| 937 |
});
|
| 938 |
const model = findManifestModel(manifest, params.modelId);
|
| 939 |
+
const contextSize = params.contextSize ?? model.defaultContext;
|
| 940 |
+
const contextPolicy = evaluateModelContextPolicy(model, contextSize, {
|
| 941 |
+
tuningScope: tuning.scope,
|
| 942 |
+
requestedBackend: params.backend,
|
| 943 |
+
flashMode: tuning.flashMode,
|
| 944 |
+
kvCacheType: tuning.kvCacheType,
|
| 945 |
+
});
|
| 946 |
+
if (!contextPolicy.allowed) {
|
| 947 |
+
throw new EngineRuntimeError(
|
| 948 |
+
'INVALID_CONTEXT_SIZE',
|
| 949 |
+
`Context size must be between 1 and ${contextPolicy.limit} for this model and runtime policy.`,
|
| 950 |
+
);
|
| 951 |
+
}
|
| 952 |
const urls = orderedShardUrls(manifest, model);
|
| 953 |
const wllama = await this.ensureWllama(wasmFlavor);
|
| 954 |
const cached = await this.inspectCachedShards(wllama, model, urls, signal);
|
|
|
|
| 977 |
shardCount: model.files.length,
|
| 978 |
shardPath: null,
|
| 979 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
const defaultThreads = Math.max(1, Math.floor((navigator.hardwareConcurrency || 1) / 2));
|
| 981 |
const threads = params.threads ?? (gate.selectedBackend === 'wasm' ? defaultThreads : 1);
|
| 982 |
if (!Number.isSafeInteger(threads) || threads <= 0) {
|
|
|
|
| 1058 |
tuningResult,
|
| 1059 |
);
|
| 1060 |
}
|
| 1061 |
+
this.loaded = {
|
| 1062 |
+
manifest,
|
| 1063 |
+
model,
|
| 1064 |
+
backend: gate.selectedBackend,
|
| 1065 |
+
tuningScope: tuning.scope,
|
| 1066 |
+
contextSize: context.n_ctx,
|
| 1067 |
+
batchSize: context.n_batch,
|
| 1068 |
+
microBatchSize: context.n_ubatch,
|
| 1069 |
+
vocabularySize: context.n_vocab,
|
| 1070 |
+
};
|
| 1071 |
emitProgress(sink, requestId, {
|
| 1072 |
phase: 'load',
|
| 1073 |
loadedBytes: model.downloadBytes,
|
|
|
|
| 1126 |
}
|
| 1127 |
throwIfAborted(signal);
|
| 1128 |
let text = '';
|
| 1129 |
+
let reasoningText = '';
|
| 1130 |
let finishReason: GenerateResult['finishReason'] = null;
|
| 1131 |
let usage: GenerateResult['usage'] = null;
|
| 1132 |
let timings: GenerateResult['timings'] = null;
|
| 1133 |
+
const tokenTrace: EngineSampledTokenTraceEntry[] | null = params.returnTokenIds === true ? [] : null;
|
| 1134 |
const streamedToolCalls = new ToolCallAccumulator();
|
| 1135 |
try {
|
| 1136 |
await this.raceWebGpuDeviceLoss('generate', loaded.backend, loaded.model, wllama.createChatCompletion({
|
|
|
|
| 1145 |
tools: params.tools as ChatCompletionTool[] | undefined,
|
| 1146 |
tool_choice: params.toolChoice,
|
| 1147 |
cache_prompt: params.cachePrompt ?? true,
|
| 1148 |
+
...(params.returnTokenIds === true
|
| 1149 |
+
? { logprobs: true, top_logprobs: TOKEN_TRACE_TOP_LOGPROBS }
|
| 1150 |
+
: {}),
|
| 1151 |
abortSignal: signal,
|
| 1152 |
timings_per_token: true,
|
| 1153 |
onData: (chunk: ChatCompletionChunk) => {
|
| 1154 |
+
if (tokenTrace !== null) appendSampledTokenTrace(tokenTrace, chunk);
|
| 1155 |
streamedToolCalls.append(chunk.choices[0]?.delta.tool_calls);
|
| 1156 |
const delta = chunk.choices[0]?.delta.content;
|
| 1157 |
const textDelta = typeof delta === 'string' ? delta : '';
|
| 1158 |
const reasoningDelta = asReasoningDelta(chunk);
|
| 1159 |
if (textDelta || reasoningDelta) {
|
| 1160 |
text += textDelta;
|
| 1161 |
+
reasoningText += reasoningDelta ?? '';
|
| 1162 |
sink({
|
| 1163 |
type: 'event',
|
| 1164 |
requestId,
|
|
|
|
| 1179 |
}
|
| 1180 |
await this.assertWebGpuAlive('generate', loaded.backend, loaded.model);
|
| 1181 |
throwIfAborted(signal);
|
| 1182 |
+
assertCompleteTokenTrace(tokenTrace, usage);
|
| 1183 |
+
const tokenIds = tokenTrace?.map((entry) => entry.selected.id) ?? null;
|
| 1184 |
const report = this.nativeLog.report();
|
| 1185 |
try {
|
| 1186 |
assertBackendPolicy(loaded.model, loaded.backend, report);
|
|
|
|
| 1201 |
error instanceof Error ? error.message : String(error),
|
| 1202 |
);
|
| 1203 |
}
|
| 1204 |
+
return { text, reasoningText, tokenIds, tokenTrace, finishReason, toolCalls, usage, timings };
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
async scoreSequence(
|
| 1208 |
+
params: ScoreSequenceParams,
|
| 1209 |
+
signal: AbortSignal,
|
| 1210 |
+
): Promise<ScoreSequenceResult> {
|
| 1211 |
+
const loaded = this.loaded;
|
| 1212 |
+
const wllama = this.wllama;
|
| 1213 |
+
if (!loaded || !wllama?.isModelLoaded()) {
|
| 1214 |
+
throw new EngineRuntimeError('MODEL_NOT_LOADED', 'Load a model before scoring a sequence.');
|
| 1215 |
+
}
|
| 1216 |
+
if (
|
| 1217 |
+
loaded.model.id !== '27b'
|
| 1218 |
+
|| loaded.backend !== 'webgpu'
|
| 1219 |
+
|| loaded.tuningScope !== 'benchmark'
|
| 1220 |
+
) {
|
| 1221 |
+
throw new EngineRuntimeError(
|
| 1222 |
+
'SCORE_SEQUENCE_UNAVAILABLE',
|
| 1223 |
+
'Teacher-forced scoring is diagnostic-only and requires the loaded 27B WebGPU benchmark path.',
|
| 1224 |
+
{
|
| 1225 |
+
modelId: loaded.model.id,
|
| 1226 |
+
backend: loaded.backend,
|
| 1227 |
+
tuningScope: loaded.tuningScope,
|
| 1228 |
+
},
|
| 1229 |
+
);
|
| 1230 |
+
}
|
| 1231 |
+
if (params.topK !== TOKEN_TRACE_TOP_LOGPROBS) {
|
| 1232 |
+
throw new EngineRuntimeError(
|
| 1233 |
+
'INVALID_SCORE_SEQUENCE',
|
| 1234 |
+
`Teacher-forced scoring requires topK=${TOKEN_TRACE_TOP_LOGPROBS}.`,
|
| 1235 |
+
);
|
| 1236 |
+
}
|
| 1237 |
+
if (!Array.isArray(params.promptTokenIds)
|
| 1238 |
+
|| params.promptTokenIds.length !== STATE_DRIFT_PROMPT_TOKENS) {
|
| 1239 |
+
throw new EngineRuntimeError(
|
| 1240 |
+
'INVALID_SCORE_SEQUENCE',
|
| 1241 |
+
`Teacher-forced scoring requires exactly ${STATE_DRIFT_PROMPT_TOKENS} rendered prompt token ids.`,
|
| 1242 |
+
);
|
| 1243 |
+
}
|
| 1244 |
+
if (!Array.isArray(params.referenceTokenIds)
|
| 1245 |
+
|| params.referenceTokenIds.length !== STATE_DRIFT_REFERENCE_TOKENS) {
|
| 1246 |
+
throw new EngineRuntimeError(
|
| 1247 |
+
'INVALID_SCORE_SEQUENCE',
|
| 1248 |
+
`Teacher-forced scoring requires exactly ${STATE_DRIFT_REFERENCE_TOKENS} reference token ids.`,
|
| 1249 |
+
);
|
| 1250 |
+
}
|
| 1251 |
+
if (params.promptTokenIds.length + params.referenceTokenIds.length > loaded.contextSize) {
|
| 1252 |
+
throw new EngineRuntimeError(
|
| 1253 |
+
'INVALID_SCORE_SEQUENCE',
|
| 1254 |
+
'The rendered prompt and fixed reference sequence exceed the loaded context.',
|
| 1255 |
+
{
|
| 1256 |
+
promptTokens: params.promptTokenIds.length,
|
| 1257 |
+
referenceTokens: params.referenceTokenIds.length,
|
| 1258 |
+
contextSize: loaded.contextSize,
|
| 1259 |
+
},
|
| 1260 |
+
);
|
| 1261 |
+
}
|
| 1262 |
+
const validateInputTokenIds = (values: readonly number[], field: string): void => {
|
| 1263 |
+
for (const [index, tokenId] of values.entries()) {
|
| 1264 |
+
if (!Number.isSafeInteger(tokenId) || tokenId < 0 || tokenId >= loaded.vocabularySize) {
|
| 1265 |
+
throw new EngineRuntimeError(
|
| 1266 |
+
'INVALID_SCORE_SEQUENCE',
|
| 1267 |
+
`${field}[${index}] is outside the loaded vocabulary.`,
|
| 1268 |
+
{ field, index, tokenId, vocabularySize: loaded.vocabularySize },
|
| 1269 |
+
);
|
| 1270 |
+
}
|
| 1271 |
+
}
|
| 1272 |
+
};
|
| 1273 |
+
validateInputTokenIds(params.promptTokenIds, 'promptTokenIds');
|
| 1274 |
+
validateInputTokenIds(params.referenceTokenIds, 'referenceTokenIds');
|
| 1275 |
+
await this.assertWebGpuAlive('score-sequence', loaded.backend, loaded.model);
|
| 1276 |
+
throwIfAborted(signal);
|
| 1277 |
+
|
| 1278 |
+
const score = async (): Promise<ScoreSequenceResult> => {
|
| 1279 |
+
const entries: ScoreSequenceResult['entries'] = [];
|
| 1280 |
+
const createRawCompletion = wllama.createCompletion.bind(wllama) as unknown as (
|
| 1281 |
+
options: Record<string, unknown>,
|
| 1282 |
+
) => Promise<unknown>;
|
| 1283 |
+
for (let index = 0; index < params.referenceTokenIds.length; index += 1) {
|
| 1284 |
+
throwIfAborted(signal);
|
| 1285 |
+
const referenceTokenId = params.referenceTokenIds[index]!;
|
| 1286 |
+
const prompt = [
|
| 1287 |
+
...params.promptTokenIds,
|
| 1288 |
+
...params.referenceTokenIds.slice(0, index),
|
| 1289 |
+
];
|
| 1290 |
+
const response = await createRawCompletion({
|
| 1291 |
+
// The pinned llama.cpp endpoint accepts a raw token-id prompt even though
|
| 1292 |
+
// upstream wllama's OAI declaration still narrows this field to strings.
|
| 1293 |
+
prompt,
|
| 1294 |
+
stream: false,
|
| 1295 |
+
max_tokens: 1,
|
| 1296 |
+
temperature: 0,
|
| 1297 |
+
top_k: 1,
|
| 1298 |
+
logprobs: TOKEN_TRACE_TOP_LOGPROBS,
|
| 1299 |
+
logit_bias: { [String(referenceTokenId)]: TEACHER_FORCE_LOGIT_BIAS },
|
| 1300 |
+
cache_prompt: index !== 0,
|
| 1301 |
+
post_sampling_probs: false,
|
| 1302 |
+
abortSignal: signal,
|
| 1303 |
+
});
|
| 1304 |
+
throwIfAborted(signal);
|
| 1305 |
+
entries.push(parseTeacherForcedResponse(
|
| 1306 |
+
response,
|
| 1307 |
+
index,
|
| 1308 |
+
referenceTokenId,
|
| 1309 |
+
loaded.vocabularySize,
|
| 1310 |
+
));
|
| 1311 |
+
}
|
| 1312 |
+
const meanNll = -entries.reduce(
|
| 1313 |
+
(sum, entry) => sum + entry.selectedReference.logprob,
|
| 1314 |
+
0,
|
| 1315 |
+
) / entries.length;
|
| 1316 |
+
const perplexity = Math.exp(meanNll);
|
| 1317 |
+
if (!Number.isFinite(meanNll) || !Number.isFinite(perplexity)) {
|
| 1318 |
+
throw new EngineRuntimeError(
|
| 1319 |
+
'INVALID_SCORE_SEQUENCE_RESPONSE',
|
| 1320 |
+
'Teacher-forced scoring produced a non-finite mean NLL or perplexity.',
|
| 1321 |
+
{ meanNll, perplexity },
|
| 1322 |
+
);
|
| 1323 |
+
}
|
| 1324 |
+
return {
|
| 1325 |
+
method: {
|
| 1326 |
+
promptMode: 'raw-token-id-prefix',
|
| 1327 |
+
maxTokensPerStep: 1,
|
| 1328 |
+
temperature: 0,
|
| 1329 |
+
topK: 1,
|
| 1330 |
+
reportedTopLogprobs: TOKEN_TRACE_TOP_LOGPROBS,
|
| 1331 |
+
logitBias: TEACHER_FORCE_LOGIT_BIAS,
|
| 1332 |
+
cachePromptFirst: false,
|
| 1333 |
+
cachePromptSubsequent: true,
|
| 1334 |
+
},
|
| 1335 |
+
entries,
|
| 1336 |
+
summary: {
|
| 1337 |
+
tokenCount: STATE_DRIFT_REFERENCE_TOKENS,
|
| 1338 |
+
meanNll,
|
| 1339 |
+
perplexity,
|
| 1340 |
+
},
|
| 1341 |
+
};
|
| 1342 |
+
};
|
| 1343 |
+
|
| 1344 |
+
try {
|
| 1345 |
+
const result = await this.raceWebGpuDeviceLoss(
|
| 1346 |
+
'score-sequence',
|
| 1347 |
+
loaded.backend,
|
| 1348 |
+
loaded.model,
|
| 1349 |
+
score(),
|
| 1350 |
+
);
|
| 1351 |
+
await this.assertWebGpuAlive('score-sequence', loaded.backend, loaded.model);
|
| 1352 |
+
throwIfAborted(signal);
|
| 1353 |
+
const report = this.nativeLog.report();
|
| 1354 |
+
try {
|
| 1355 |
+
assertBackendPolicy(loaded.model, loaded.backend, report);
|
| 1356 |
+
} catch (error) {
|
| 1357 |
+
await this.unload();
|
| 1358 |
+
throw new EngineRuntimeError(
|
| 1359 |
+
'BACKEND_TRIPWIRE',
|
| 1360 |
+
error instanceof Error ? error.message : String(error),
|
| 1361 |
+
report,
|
| 1362 |
+
);
|
| 1363 |
+
}
|
| 1364 |
+
return result;
|
| 1365 |
+
} catch (error) {
|
| 1366 |
+
if (error instanceof EngineRuntimeError && error.code === 'WEBGPU_DEVICE_LOST') throw error;
|
| 1367 |
+
await this.assertWebGpuAlive('score-sequence', loaded.backend, loaded.model);
|
| 1368 |
+
throw error;
|
| 1369 |
+
}
|
| 1370 |
}
|
| 1371 |
|
| 1372 |
async backendReport(): Promise<BackendReport> {
|
src/engine/worker.ts
CHANGED
|
@@ -137,6 +137,14 @@ async function handleRequest(request: EngineRequest): Promise<void> {
|
|
| 137 |
)),
|
| 138 |
);
|
| 139 |
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
case 'abort': {
|
| 141 |
const operation = active.get(request.params.targetRequestId);
|
| 142 |
const aborted = operation !== undefined && !operation.controller.signal.aborted;
|
|
|
|
| 137 |
)),
|
| 138 |
);
|
| 139 |
return;
|
| 140 |
+
case 'scoreSequence':
|
| 141 |
+
postSuccess(
|
| 142 |
+
request,
|
| 143 |
+
await runExclusive(request.requestId, (signal) => (
|
| 144 |
+
runtime.scoreSequence(request.params, signal)
|
| 145 |
+
)),
|
| 146 |
+
);
|
| 147 |
+
return;
|
| 148 |
case 'abort': {
|
| 149 |
const operation = active.get(request.params.targetRequestId);
|
| 150 |
const aborted = operation !== undefined && !operation.controller.signal.aborted;
|
vendor/wllama-bonsai/PATCH.diff
CHANGED
|
@@ -23,7 +23,7 @@ index f1496a4..8883324 100644
|
|
| 23 |
GLUE_FIELD(int, n_threads)
|
| 24 |
GLUE_FIELD_NULLABLE(str, model_alias)
|
| 25 |
diff --git a/cpp/wllama-context.h b/cpp/wllama-context.h
|
| 26 |
-
index 0dafe6d..
|
| 27 |
--- a/cpp/wllama-context.h
|
| 28 |
+++ b/cpp/wllama-context.h
|
| 29 |
@@ -19,6 +19,7 @@
|
|
@@ -34,7 +34,7 @@ index 0dafe6d..c4b46f9 100644
|
|
| 34 |
|
| 35 |
#include "ggml-cpu.h"
|
| 36 |
#include "ggml-backend.h"
|
| 37 |
-
@@ -300,
|
| 38 |
server_task task = server_task(SERVER_TASK_TYPE_COMPLETION);
|
| 39 |
task.id = rd->get_new_id();
|
| 40 |
task.index = 0;
|
|
@@ -43,7 +43,31 @@ index 0dafe6d..c4b46f9 100644
|
|
| 43 |
vocab,
|
| 44 |
params,
|
| 45 |
meta->slot_n_ctx,
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
params.embedding = req.embeddings.value;
|
| 48 |
if (req.n_batch.not_null())
|
| 49 |
params.n_batch = req.n_batch.value;
|
|
@@ -52,7 +76,7 @@ index 0dafe6d..c4b46f9 100644
|
|
| 52 |
if (req.n_parallel.not_null())
|
| 53 |
params.n_parallel = req.n_parallel.value;
|
| 54 |
if (req.pooling_type.not_null())
|
| 55 |
-
@@ -568,6 +
|
| 56 |
// load model
|
| 57 |
llama_backend_init();
|
| 58 |
llama_numa_init(params.numa);
|
|
@@ -101,6 +125,18 @@ index 08f0d87..c1001da 100644
|
|
| 101 |
n_ctx: number;
|
| 102 |
n_threads: number;
|
| 103 |
model_alias?: string | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
diff --git a/src/types/types.ts b/src/types/types.ts
|
| 105 |
index 2b3cca7..e91b395 100644
|
| 106 |
--- a/src/types/types.ts
|
|
@@ -235,3 +271,17 @@ index ce74b2b..1796ca1 100644
|
|
| 235 |
if (!this.compat) {
|
| 236 |
this.logger().warn(
|
| 237 |
'Not using compat mode' +
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
GLUE_FIELD(int, n_threads)
|
| 24 |
GLUE_FIELD_NULLABLE(str, model_alias)
|
| 25 |
diff --git a/cpp/wllama-context.h b/cpp/wllama-context.h
|
| 26 |
+
index 0dafe6d..d261dfe 100644
|
| 27 |
--- a/cpp/wllama-context.h
|
| 28 |
+++ b/cpp/wllama-context.h
|
| 29 |
@@ -19,6 +19,7 @@
|
|
|
|
| 34 |
|
| 35 |
#include "ggml-cpu.h"
|
| 36 |
#include "ggml-backend.h"
|
| 37 |
+
@@ -300,16 +301,28 @@ struct wllama_context
|
| 38 |
server_task task = server_task(SERVER_TASK_TYPE_COMPLETION);
|
| 39 |
task.id = rd->get_new_id();
|
| 40 |
task.index = 0;
|
|
|
|
| 43 |
vocab,
|
| 44 |
params,
|
| 45 |
meta->slot_n_ctx,
|
| 46 |
+
meta->logit_bias_eog,
|
| 47 |
+
body);
|
| 48 |
+
task.params.res_type = res_type;
|
| 49 |
+
- task.cli_prompt = prompt;
|
| 50 |
+
- task.cli_files = files;
|
| 51 |
+
- task.cli = true;
|
| 52 |
+
+ if (prompt.is_array())
|
| 53 |
+
+ {
|
| 54 |
+
+ auto tokenized_prompts = tokenize_input_prompts(vocab, nullptr, prompt, true, true);
|
| 55 |
+
+ if (tokenized_prompts.size() != 1)
|
| 56 |
+
+ {
|
| 57 |
+
+ throw app_exception("wllama completion accepts exactly one token-id prompt");
|
| 58 |
+
+ }
|
| 59 |
+
+ task.tokens = std::move(tokenized_prompts[0]);
|
| 60 |
+
+ }
|
| 61 |
+
+ else
|
| 62 |
+
+ {
|
| 63 |
+
+ task.cli_prompt = prompt.get<std::string>();
|
| 64 |
+
+ task.cli_files = files;
|
| 65 |
+
+ task.cli = true;
|
| 66 |
+
+ }
|
| 67 |
+
|
| 68 |
+
rd->post_task({std::move(task)});
|
| 69 |
+
}
|
| 70 |
+
@@ -415,6 +428,8 @@ struct wllama_context
|
| 71 |
params.embedding = req.embeddings.value;
|
| 72 |
if (req.n_batch.not_null())
|
| 73 |
params.n_batch = req.n_batch.value;
|
|
|
|
| 76 |
if (req.n_parallel.not_null())
|
| 77 |
params.n_parallel = req.n_parallel.value;
|
| 78 |
if (req.pooling_type.not_null())
|
| 79 |
+
@@ -568,6 +583,24 @@ struct wllama_context
|
| 80 |
// load model
|
| 81 |
llama_backend_init();
|
| 82 |
llama_numa_init(params.numa);
|
|
|
|
| 125 |
n_ctx: number;
|
| 126 |
n_threads: number;
|
| 127 |
model_alias?: string | undefined;
|
| 128 |
+
diff --git a/src/types/oai-compat.ts b/src/types/oai-compat.ts
|
| 129 |
+
index dbeeaa2..fd41519 100644
|
| 130 |
+
--- a/src/types/oai-compat.ts
|
| 131 |
+
+++ b/src/types/oai-compat.ts
|
| 132 |
+
@@ -135,6 +135,7 @@ export type ChatCompletionParams = {
|
| 133 |
+
// Response types----------
|
| 134 |
+
|
| 135 |
+
export interface ChatCompletionLogprob {
|
| 136 |
+
+ id: number;
|
| 137 |
+
token: string;
|
| 138 |
+
logprob: number;
|
| 139 |
+
bytes: number[] | null;
|
| 140 |
diff --git a/src/types/types.ts b/src/types/types.ts
|
| 141 |
index 2b3cca7..e91b395 100644
|
| 142 |
--- a/src/types/types.ts
|
|
|
|
| 271 |
if (!this.compat) {
|
| 272 |
this.logger().warn(
|
| 273 |
'Not using compat mode' +
|
| 274 |
+
diff --git a/src/workers-code/llama-cpp.js b/src/workers-code/llama-cpp.js
|
| 275 |
+
index be62030..9f5144a 100644
|
| 276 |
+
--- a/src/workers-code/llama-cpp.js
|
| 277 |
+
+++ b/src/workers-code/llama-cpp.js
|
| 278 |
+
@@ -468,6 +468,9 @@ onmessage = async (e) => {
|
| 279 |
+
);
|
| 280 |
+
inputBuffer.set(argEncodedMsg, 0);
|
| 281 |
+
const outputPtr = await wllamaAction(argAction, inputPtr);
|
| 282 |
+
+ if (!outputPtr) {
|
| 283 |
+
+ throw new Error(`wllama_action("${argAction}") returned a null pointer`);
|
| 284 |
+
+ }
|
| 285 |
+
// length of output buffer is written at the first 4 bytes of input buffer
|
| 286 |
+
const outputLen = new Uint32Array(
|
| 287 |
+
getHeapU8().buffer,
|
vendor/wllama-bonsai/SOURCE.json
CHANGED
|
@@ -4,19 +4,21 @@
|
|
| 4 |
"wllamaRevision": "912c18b75d4358c1405a64646b8dbe43a205943b",
|
| 5 |
"llamaCppRevision": "00fa7cb284cbf133fc426733bd64238a3588a33e",
|
| 6 |
"license": "MIT",
|
| 7 |
-
"patch": "Expose offload_token_embedding and n_ubatch; place token_embd.weight on WebGPU whenever GPU layers are requested; fail loudly when the requested WebGPU buffer is unavailable; add an opt-in forceCompat selector for controlled benchmark A/B runs.",
|
| 8 |
"patchSet": {
|
| 9 |
"format": "git-diff-binary",
|
| 10 |
"path": "vendor/wllama-bonsai/PATCH.diff",
|
| 11 |
-
"bytes":
|
| 12 |
-
"sha256": "
|
| 13 |
"files": [
|
| 14 |
"CMakeLists.txt",
|
| 15 |
"cpp/glue.hpp",
|
| 16 |
"cpp/wllama-context.h",
|
| 17 |
"src/glue/messages.ts",
|
|
|
|
| 18 |
"src/types/types.ts",
|
| 19 |
-
"src/wllama.ts"
|
|
|
|
| 20 |
]
|
| 21 |
},
|
| 22 |
"build": {
|
|
@@ -28,18 +30,18 @@
|
|
| 28 |
"files": [
|
| 29 |
{
|
| 30 |
"path": "vendor/wllama-bonsai/esm/index.js",
|
| 31 |
-
"bytes":
|
| 32 |
-
"sha256": "
|
| 33 |
},
|
| 34 |
{
|
| 35 |
"path": "public/wasm/wllama.wasm",
|
| 36 |
-
"bytes":
|
| 37 |
-
"sha256": "
|
| 38 |
},
|
| 39 |
{
|
| 40 |
"path": "public/wasm/wllama-compat.wasm",
|
| 41 |
-
"bytes":
|
| 42 |
-
"sha256": "
|
| 43 |
},
|
| 44 |
{
|
| 45 |
"path": "public/wasm/wllama-compat.js",
|
|
|
|
| 4 |
"wllamaRevision": "912c18b75d4358c1405a64646b8dbe43a205943b",
|
| 5 |
"llamaCppRevision": "00fa7cb284cbf133fc426733bd64238a3588a33e",
|
| 6 |
"license": "MIT",
|
| 7 |
+
"patch": "Expose offload_token_embedding and n_ubatch; place token_embd.weight on WebGPU whenever GPU layers are requested; fail loudly when the requested WebGPU buffer is unavailable; add an opt-in forceCompat selector for controlled benchmark A/B runs; type sampled token ids in OAI logprob payloads; accept one raw token-id completion prompt and fail loudly on null action responses.",
|
| 8 |
"patchSet": {
|
| 9 |
"format": "git-diff-binary",
|
| 10 |
"path": "vendor/wllama-bonsai/PATCH.diff",
|
| 11 |
+
"bytes": 11052,
|
| 12 |
+
"sha256": "27b96fa48c6fd66b44fe70e83cc775902e29ec5877261c7eb04fecabc529d2f9",
|
| 13 |
"files": [
|
| 14 |
"CMakeLists.txt",
|
| 15 |
"cpp/glue.hpp",
|
| 16 |
"cpp/wllama-context.h",
|
| 17 |
"src/glue/messages.ts",
|
| 18 |
+
"src/types/oai-compat.ts",
|
| 19 |
"src/types/types.ts",
|
| 20 |
+
"src/wllama.ts",
|
| 21 |
+
"src/workers-code/llama-cpp.js"
|
| 22 |
]
|
| 23 |
},
|
| 24 |
"build": {
|
|
|
|
| 30 |
"files": [
|
| 31 |
{
|
| 32 |
"path": "vendor/wllama-bonsai/esm/index.js",
|
| 33 |
+
"bytes": 365536,
|
| 34 |
+
"sha256": "10c3811776e34092a225632929b97046af4215a4c91156169105c886be5cba5d"
|
| 35 |
},
|
| 36 |
{
|
| 37 |
"path": "public/wasm/wllama.wasm",
|
| 38 |
+
"bytes": 8017784,
|
| 39 |
+
"sha256": "b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a"
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"path": "public/wasm/wllama-compat.wasm",
|
| 43 |
+
"bytes": 14865338,
|
| 44 |
+
"sha256": "ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c"
|
| 45 |
},
|
| 46 |
{
|
| 47 |
"path": "public/wasm/wllama-compat.js",
|
vendor/wllama-bonsai/esm/index.cjs
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
vendor/wllama-bonsai/esm/index.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
vendor/wllama-bonsai/esm/index.min.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
vendor/wllama-bonsai/esm/index.min.js.map
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
vendor/wllama-bonsai/esm/types/oai-compat.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export type ChatCompletionParams = {
|
|
| 96 |
timings_per_token?: boolean;
|
| 97 |
} & SamplingParams;
|
| 98 |
export interface ChatCompletionLogprob {
|
|
|
|
| 99 |
token: string;
|
| 100 |
logprob: number;
|
| 101 |
bytes: number[] | null;
|
|
|
|
| 96 |
timings_per_token?: boolean;
|
| 97 |
} & SamplingParams;
|
| 98 |
export interface ChatCompletionLogprob {
|
| 99 |
+
id: number;
|
| 100 |
token: string;
|
| 101 |
logprob: number;
|
| 102 |
bytes: number[] | null;
|
vendor/wllama-bonsai/esm/workers-code/generated.d.ts
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|