Spaces:
Running
Running
Commit Β·
e26c434
0
Parent(s):
sync from github@30b8f47
Browse filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- .gitattributes +4 -0
- 404.html +29 -0
- README.md +120 -0
- _headers +8 -0
- _redirects +16 -0
- agent-host.html +78 -0
- assets/agent-host-eDUnNJBhJx.js +84 -0
- assets/cache-probe-BYX_n9OPRB.js +1 -0
- assets/chat-flow-eTAzKAlIYe.js +6 -0
- assets/chat-ui-CQAxV1po6S.js +23 -0
- assets/engine-core-CLR3cxsqf4.js +0 -0
- assets/feats-dlBfBxqpI8.js +1 -0
- assets/index-BqNbylMY24.js +70 -0
- assets/landing-chat-Bo__3GxplQ.js +49 -0
- assets/landing-room-BSBnFYP4bg.js +17 -0
- assets/loading-ui-CjCwSRuXvh.js +7 -0
- assets/mascot-BCKWddkWrV.js +307 -0
- assets/model-registry-BoK-ObgACo.js +1 -0
- assets/model-select-DhhV9mCi0T.js +39 -0
- assets/preload-helper-ckwbz45pO3.js +1 -0
- assets/room-host-C6R3zseuU5.js +1 -0
- assets/share-TXmFQ6ZAWJ.js +13 -0
- assets/validate-D2Aic8RAjT.js +16 -0
- assets/variants-De-VVc9BRz.js +1 -0
- assets/weight-loader-ByOLMd9f5C.js +2 -0
- assets/zero-tvm-nxGrkTTcQf.js +29 -0
- chat-ui.css +523 -0
- docs.html +1316 -0
- entrance/backring.svg +18 -0
- entrance/banner.svg +13 -0
- entrance/bg.svg +27 -0
- entrance/border.svg +6 -0
- entrance/column.svg +20 -0
- entrance/corner.svg +11 -0
- entrance/divider.svg +13 -0
- entrance/pframe.svg +7 -0
- entrance/ring.svg +22 -0
- entrance/spires.svg +31 -0
- favicon.svg +7 -0
- fonts.css +131 -0
- fonts/f1-fraunce.woff2 +3 -0
- fonts/f2-fraunce.woff2 +3 -0
- fonts/f3-fraunce.woff2 +3 -0
- fonts/f4-plinean.woff2 +3 -0
- fonts/f5-plinean.woff2 +3 -0
- fonts/f6-plineanmono.woff2 +3 -0
- fonts/f7-plineanmono.woff2 +3 -0
- index.html +151 -0
- landing.css +1442 -0
- og.png +3 -0
.gitattributes
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
404.html
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<!-- Presence of this file switches Cloudflare Pages from SPA fallback (200 +
|
| 3 |
+
index.html for any miss) to real 404 responses. That matters beyond
|
| 4 |
+
cosmetics: a miss under /assets/* used to return the landing page WITH
|
| 5 |
+
that path's immutable cache header, and one such response got cached at
|
| 6 |
+
the edge for a year (2026-08-06). A real 404 is not cacheable that way.
|
| 7 |
+
Every route on this site is a real file, so nothing relied on the
|
| 8 |
+
fallback. -->
|
| 9 |
+
<html lang="en">
|
| 10 |
+
<head>
|
| 11 |
+
<meta charset="UTF-8">
|
| 12 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 13 |
+
<title>404 β Zero-TVM</title>
|
| 14 |
+
<meta name="robots" content="noindex">
|
| 15 |
+
<style>
|
| 16 |
+
body { background:#0a0a0f; color:#ccc; font:15px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;
|
| 17 |
+
display:flex; align-items:center; justify-content:center; min-height:100vh; margin:0 }
|
| 18 |
+
main { text-align:center; padding:2rem }
|
| 19 |
+
h1 { font-size:1.2rem; color:#eee; font-weight:600 }
|
| 20 |
+
a { color:#6ea8ff }
|
| 21 |
+
</style>
|
| 22 |
+
</head>
|
| 23 |
+
<body>
|
| 24 |
+
<main>
|
| 25 |
+
<h1>404 β no such page</h1>
|
| 26 |
+
<p><a href="/">zerotvm.com</a> β pick a model, it runs in your tab.</p>
|
| 27 |
+
</main>
|
| 28 |
+
</body>
|
| 29 |
+
</html>
|
README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Zero-TVM
|
| 3 |
+
emoji: π§
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: static
|
| 7 |
+
pinned: true
|
| 8 |
+
license: mit
|
| 9 |
+
thumbnail: https://huggingface.co/spaces/abgunaydin/zero-tvm/resolve/main/og.png
|
| 10 |
+
short_description: Phi-3 to a 35B MoE in the browser, on hand-written WGSL
|
| 11 |
+
tags:
|
| 12 |
+
- webgpu
|
| 13 |
+
- llm
|
| 14 |
+
- inference
|
| 15 |
+
- on-device
|
| 16 |
+
- wgsl
|
| 17 |
+
models:
|
| 18 |
+
- microsoft/Phi-3-mini-4k-instruct
|
| 19 |
+
- Qwen/Qwen3-4B
|
| 20 |
+
- Qwen/Qwen3.5-4B
|
| 21 |
+
- meta-llama/Llama-3.2-1B-Instruct
|
| 22 |
+
- mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC
|
| 23 |
+
- mlc-ai/Qwen3-4B-q4f16_1-MLC
|
| 24 |
+
- mlc-ai/Qwen3.5-4B-q4f16_1-MLC
|
| 25 |
+
- mlx-community/Qwen3-4B-4bit
|
| 26 |
+
- mlx-community/Qwen3-30B-A3B-4bit
|
| 27 |
+
- mlx-community/Llama-3.2-1B-Instruct-4bit
|
| 28 |
+
- lmstudio-community/Qwen3.6-35B-A3B-MLX-4bit
|
| 29 |
+
- abgunaydin/Qwen3.6-35B-A3B-MLX-q3exp
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
# Zero-TVM
|
| 33 |
+
|
| 34 |
+
Models from a 3.8B dense to a 35B sparse MoE, running in the browser on hand-written WGSL. No TVM, no ONNX, no WASM runtime.
|
| 35 |
+
|
| 36 |
+
The standard way to run a modern LLM in a browser is [WebLLM / MLC-LLM](https://webllm.mlc.ai/), which ships an Apache-TVM compiler pipeline that autotunes its WGSL β 85 shaders captured across a session, around 11 of them on the decode path. This Space replaces that entire stack with **10 kernel roles** of hand-written WGSL β and runs models WebLLM ships (same weights, measured faster) as well as one it does not: Qwen3.6-35B-A3B, a 256-expert sparse MoE.
|
| 37 |
+
|
| 38 |
+
The whole forward pass β 32 transformer layers, paged KV cache, int4-dequant matmul, RoPE, fused FFN, RMSNorm, paged attention, argmax sampling β is readable end-to-end in a single sitting. That is the point.
|
| 39 |
+
|
| 40 |
+
## Headline result
|
| 41 |
+
|
| 42 |
+
Measured on an Apple M2 Max (Chrome 150, WebLLM v0.2.80, identical Phi-3-mini-q4f16_1 weights, same-session head-to-head, 2026-07-30). Two numbers, always both: **total** is wall-clock throughput with prefill included (both engines doing identical work), **decode** excludes prefill.
|
| 43 |
+
|
| 44 |
+
| | WebLLM (TVM) | Zero-TVM |
|
| 45 |
+
|---|---|---|
|
| 46 |
+
| WGSL kernels (decode path) | **~11 TVM-generated** (85 captured across a session) | **10 hand-written roles** |
|
| 47 |
+
| Dispatches per decode step | **342** | **260** default (228 with `?splitk=0`) |
|
| 48 |
+
| Total throughput (prefill + decode) | **59.95 tok/s** | **69.55 tok/s** β **+16.0%** |
|
| 49 |
+
| Decode only | **63.23 tok/s** (self-reported) | **83.10 tok/s** β **+31.4%** |
|
| 50 |
+
| Time to first token (~35-tok prompt) | **~150 ms** (implied) | **291 ms** β *WebLLM wins this one* |
|
| 51 |
+
| JS bundle (excl. weights) | **~6.0 MB** / ~2.2 MB gz | **~460 kB** / ~126 kB gz |
|
| 52 |
+
|
| 53 |
+
**The hand-written kernels don't just keep up with the autotuning compiler β on this hardware they beat it on throughput.** For decoder-only LLMs of this shape, most of the compiler's complexity budget isn't buying much β the expensive parts are matmul, attention, and int4 dequant. Everything else is plumbing, and ten hand-written roles cover it.
|
| 54 |
+
|
| 55 |
+
**The gap grows with how recent the architecture is** β +16% / +31% (total / decode) on 2024's Phi-3, +32% / +58% on 2025's Qwen3-4B, +100% / +114% on 2026's Qwen3.5-4B. Consistent with compiler stacks having had less time to tune newer architectures; an observation across three points on one machine, not a proven law.
|
| 56 |
+
|
| 57 |
+
**Honest negative, published not buried:** WebLLM reaches the first token faster than we do on short prompts (~150 ms implied vs our 291 ms on Phi-3; 453 ms on Qwen3-4B). We win sustained throughput and lose the first-token sprint on short inputs β our chunked prefill is strong on long ones (202 tok/s at 816 tokens) and prefix reuse makes follow-up turns free.
|
| 58 |
+
|
| 59 |
+
Exact medians, full methodology, and the optimization experiments that were measured and *dropped* live in [BENCH.md](https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md). (An earlier measurement on an M2 Pro with an older, buggier engine read 22% *behind* β that history is preserved there too, not retconned. So are two 2026-07-29 Qwen pairs that a bench-harness defect made non-comparable; they are marked withdrawn with the reason, not deleted.)
|
| 60 |
+
|
| 61 |
+
## Run it
|
| 62 |
+
|
| 63 |
+
The Space opens on a character-select entrance: pick a model and it loads and chats in place. [`zero-tvm.html`](./zero-tvm.html) is the same chat as a direct link, if you want to deep-link a specific model.
|
| 64 |
+
|
| 65 |
+
Picking a character starts the download immediately β the sheet shows the weight size before you click, and there is no second confirmation. It streams ~2 GB of Phi-3-mini-q4f16_1 weights from the Hugging Face mirror at [`mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC`](https://huggingface.co/mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC) into OPFS (Origin Private File System).
|
| 66 |
+
|
| 67 |
+
Subsequent loads are instant β the weights stay in OPFS, so returning to a model you have already downloaded starts it straight away.
|
| 68 |
+
|
| 69 |
+
**Requirements**: Chrome / Edge with WebGPU enabled and the `shader-f16` feature available (default on macOS Apple-Silicon, enabled on most modern Windows / Linux GPUs).
|
| 70 |
+
|
| 71 |
+
## Models
|
| 72 |
+
|
| 73 |
+
All three pairs re-measured 2026-07-30 under the corrected protocol β same session, identical local weight bytes, both engines paying a full prefill on every run, total and decode reported side by side.
|
| 74 |
+
|
| 75 |
+
- **Phi-3-mini-4k-instruct (3.8B, q4f16_1)** β the default; every headline number above is Phi-3, no URL flag needed. Zero-TVM **69.55 tok/s total** (TTFT 291 ms, decode 83.10) vs WebLLM 0.2.80's **59.95 tok/s total** (self-reported decode 63.23) β **+16.0% total, +31.4% decode**.
|
| 76 |
+
- **Qwen3-4B (q4f16_1)** β append `?model=qwen3` to `zero-tvm.html` or `validate.html`; weights stream from [`mlc-ai/Qwen3-4B-q4f16_1-MLC`](https://huggingface.co/mlc-ai/Qwen3-4B-q4f16_1-MLC) (~2.3 GB). A port on the spec-parameterized engine: GQA 32/8, per-head QK-norm, byte-level BPE, tied lm_head, with the tuning round's fused post-matmul chain (`qk_norm_rope_append`, 8 dispatches/layer) and K%512 wide loads. Zero-TVM **59.85 tok/s total** (TTFT 453 ms, decode 75.49) vs WebLLM 0.2.84's prebuilt Qwen3-4B at **45.46 tok/s total** (self-reported decode 47.77) β **+31.7% total, +58.0% decode**. This is the model where WebLLM most clearly beats us on first-token latency (~150 ms implied vs our 453 ms). The previously published "75.7 vs 43.8, +73%" pair is **withdrawn** β a bench-harness defect had stopped our half paying prefill; and the 2026-07-28 pair (25.43 vs 14.15) was a degraded session that did not reproduce. Both are kept as dated history with the reason in [BENCH.md](https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md).
|
| 77 |
+
- **Qwen3.5-4B (q4f16_1)** β append `?model=qwen35` to `zero-tvm.html` or `validate.html`; weights stream from [`mlc-ai/Qwen3.5-4B-q4f16_1-MLC`](https://huggingface.co/mlc-ai/Qwen3.5-4B-q4f16_1-MLC) (~2.6 GB). The first *hybrid* on the engine: 24 gated-DeltaNet (linear-attention) layers + 8 gated-attention layers (GQA 16/4, head_dim 256, partial RoPE, sigmoid attention gate) β to our knowledge the first hand-written-kernel int4 gated-DeltaNet hybrid in a browser. Zero-TVM **65.28 tok/s total** (TTFT 171 ms, decode 73.30) vs WebLLM 0.2.84's prebuilt Qwen3.5-4B at **32.56 tok/s total** (self-reported decode 34.32) β **+100.5% total, +113.6% decode**, and the one model where first-token latency is a wash rather than a loss. The GDN decode kernels are still scalar (non-subgroup), so the decode number is a floor; prefill runs in chunks of β€64 (202 tok/s on an 816-token prompt) and every model reuses its cross-turn prefix (turn-3 first token 14.3 s β 0.19 s, logits verified bit-identical to a fresh prefill). The previously published "65.7 vs 34.0, +93%" cross-check is **withdrawn** (same harness defect); the earlier 53.07/32.36 (+64%) and 47.99/31.99 (+50%) pairs were like-for-like but are superseded. One machine, one pair each; caveats in [BENCH.md](https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md).
|
| 78 |
+
|
| 79 |
+
## Pages in this Space
|
| 80 |
+
|
| 81 |
+
- [`index.html`](./index.html) β the entrance: pick a model, chat in place (start here)
|
| 82 |
+
- [`zero-tvm.html`](./zero-tvm.html) β the chat surface as a direct link
|
| 83 |
+
- [`validate.html`](./validate.html) β multi-prompt smoke test
|
| 84 |
+
- [`docs.html`](./docs.html) β annotated reference, including the kernel walkthrough
|
| 85 |
+
|
| 86 |
+
Six pages were removed on 2026-08-14 β `architecture`, `demo`, `compiler-chat`,
|
| 87 |
+
`dump`, `shaders`, `webllm-bench`. Three of them started multi-gigabyte
|
| 88 |
+
downloads on page load. `docs.html` carries what the first two explained.
|
| 89 |
+
|
| 90 |
+
## URL flags
|
| 91 |
+
|
| 92 |
+
`zero-tvm.html` accepts query flags for A/B-ing shader variants without rebuilding:
|
| 93 |
+
|
| 94 |
+
- `?sg=0` β disable all subgroup shaders (argmax / attention / QKV matmul)
|
| 95 |
+
- `?sgqkv=0` / `?sgattn=0` / `?sgargmax=0` β disable one at a time
|
| 96 |
+
- `?qkvtile=1` / `?qkvtile2=1` β opt into tiled QKV variants
|
| 97 |
+
- `?ffnsg=1` β opt into the tiled-subgroup fused FFN
|
| 98 |
+
- `?kv8=1` β opt into the int8 KV cache path
|
| 99 |
+
|
| 100 |
+
## How it relates to a published paper
|
| 101 |
+
|
| 102 |
+
Zero-TVM is the LLM-decoding application of a broader thesis: **per-dispatch overhead is the dominant tax on browser GPU compute, and most workloads collapse from "needs a server" to "runs in a browser tab" once you fuse the loop.** Two preprints document the underlying technique:
|
| 103 |
+
|
| 104 |
+
- [Single-Kernel Fusion for Sequential Fitness Evaluation via WebGPU Compute Shaders](https://doi.org/10.5281/zenodo.19342888) (the foundational result)
|
| 105 |
+
- [Single-Kernel Fusion for Autoregressive Transformer Decoding via WebGPU Compute Shaders](https://doi.org/10.5281/zenodo.19344277) (the LLM application β the work this Space embodies)
|
| 106 |
+
|
| 107 |
+
## Source / canonical site
|
| 108 |
+
|
| 109 |
+
- **GitHub**: [github.com/abgnydn/zero-tvm](https://github.com/abgnydn/zero-tvm)
|
| 110 |
+
- **Canonical site**: [zerotvm.com](https://zerotvm.com)
|
| 111 |
+
- **Research umbrella**: [kernelfusion.dev](https://kernelfusion.dev)
|
| 112 |
+
- **Companion benchmarks**: [gpubench.dev](https://gpubench.dev)
|
| 113 |
+
|
| 114 |
+
## License
|
| 115 |
+
|
| 116 |
+
MIT. Reference comparison code (WebLLM compiler-chat path) belongs to its respective authors and is governed by their licenses.
|
| 117 |
+
|
| 118 |
+
## Author
|
| 119 |
+
|
| 120 |
+
Ahmet BarΔ±Ε GΓΌnaydΔ±n Β· [barisgunaydin.com](https://barisgunaydin.com) Β· senior full-stack consultant by day, independent researcher by night.
|
_headers
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Cloudflare Pages headers (migrated from the old vercel.json).
|
| 2 |
+
# Cross-origin isolation is required for SharedArrayBuffer (WebGPU itself does not need it).
|
| 3 |
+
/*
|
| 4 |
+
Cross-Origin-Opener-Policy: same-origin
|
| 5 |
+
Cross-Origin-Embedder-Policy: credentialless
|
| 6 |
+
|
| 7 |
+
/assets/*
|
| 8 |
+
Cache-Control: public, max-age=31536000, immutable
|
_redirects
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Six pages deleted in the 2026-08-14 redesign, five of them in the sitemap
|
| 2 |
+
# Google is crawling at priority 0.6-0.9. Without these, every bookmark, README
|
| 3 |
+
# link and search result for them lands on a bare 404 and the accumulated link
|
| 4 |
+
# equity is dropped instead of passed to the successor page.
|
| 5 |
+
/demo / 301
|
| 6 |
+
/demo.html / 301
|
| 7 |
+
/architecture /docs 301
|
| 8 |
+
/architecture.html /docs 301
|
| 9 |
+
/shaders /docs#shaders 301
|
| 10 |
+
/shaders.html /docs#shaders 301
|
| 11 |
+
/dump /docs 301
|
| 12 |
+
/dump.html /docs 301
|
| 13 |
+
/compiler-chat /zero-tvm 301
|
| 14 |
+
/compiler-chat.html /zero-tvm 301
|
| 15 |
+
/webllm-bench /docs#vs-webllm 301
|
| 16 |
+
/webllm-bench.html /docs#vs-webllm 301
|
agent-host.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<script type="module" crossorigin src="/assets/agent-host-eDUnNJBhJx.js"></script>
|
| 3 |
+
<link rel="modulepreload" crossorigin href="/assets/model-registry-BoK-ObgACo.js">
|
| 4 |
+
<link rel="modulepreload" crossorigin href="/assets/preload-helper-ckwbz45pO3.js">
|
| 5 |
+
<link rel="modulepreload" crossorigin href="/assets/weight-loader-ByOLMd9f5C.js">
|
| 6 |
+
<link rel="modulepreload" crossorigin href="/assets/model-select-DhhV9mCi0T.js">
|
| 7 |
+
<link rel="modulepreload" crossorigin href="/assets/variants-De-VVc9BRz.js">
|
| 8 |
+
<link rel="modulepreload" crossorigin href="/assets/engine-core-CLR3cxsqf4.js">
|
| 9 |
+
<link rel="modulepreload" crossorigin href="/assets/loading-ui-CjCwSRuXvh.js">
|
| 10 |
+
<meta charset="utf-8"><title>zero-tvm Β· agent host</title>
|
| 11 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 12 |
+
<!-- Not a public destination: it needs a local agent-server on 127.0.0.1:8017,
|
| 13 |
+
so a visitor arriving from search pays for weights and gets "bootingβ¦"
|
| 14 |
+
forever. share.html and 404.html already carry this; this page did not,
|
| 15 |
+
while robots.txt is Allow: / β so it was indexable. -->
|
| 16 |
+
<meta name="robots" content="noindex">
|
| 17 |
+
<!-- tokens.css FIRST: chat-ui.css is an alias layer over it, so without this
|
| 18 |
+
14 custom properties were undefined here β including --accent, which made
|
| 19 |
+
`outline: 2px solid var(--accent)` invalid at computed-value time and so
|
| 20 |
+
suppressed the browser focus ring page-wide with nothing replacing it. -->
|
| 21 |
+
<link rel="stylesheet" href="/tokens.css">
|
| 22 |
+
<link rel="stylesheet" href="/chat-ui.css">
|
| 23 |
+
<style>
|
| 24 |
+
body { font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; margin: 0; padding: 2rem;
|
| 25 |
+
background: #0b0d10; color: #d6dbe1; }
|
| 26 |
+
h1 { font-size: 1.1rem; margin: 0 0 .25rem; letter-spacing: .02em }
|
| 27 |
+
.sub { color: #7d8590; margin-bottom: 1.5rem }
|
| 28 |
+
.card { border: 1px solid #21262d; border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 1rem;
|
| 29 |
+
background: #0f1216 }
|
| 30 |
+
#status { font-weight: 600 }
|
| 31 |
+
#status.ok { color: #3fb950 } #status.err { color: #f85149 }
|
| 32 |
+
#log { white-space: pre-wrap; color: #8b949e; font-size: 12.5px; max-height: 22rem; overflow: auto;
|
| 33 |
+
margin: 0 }
|
| 34 |
+
code { background: #161b22; padding: .1rem .35rem; border-radius: 4px; color: #d6dbe1 }
|
| 35 |
+
.warn { color: #d29922 }
|
| 36 |
+
#progress-wrap { display: none }
|
| 37 |
+
#progress-wrap.active { display: block }
|
| 38 |
+
#progress-bar { height: 6px; background: #d29922; width: 0%; border-radius: 3px; transition: width .2s }
|
| 39 |
+
#progress-status { margin-top: .6rem; font-size: 13px }
|
| 40 |
+
#progress-detail { color: #7d8590; font-size: 12px }
|
| 41 |
+
#progress-log { white-space: pre-wrap; color: #8b949e; font-size: 12px; max-height: 14rem;
|
| 42 |
+
overflow: auto; margin: .6rem 0 0 }
|
| 43 |
+
</style>
|
| 44 |
+
<h1>zero-tvm Β· agent host</h1>
|
| 45 |
+
<div class="sub">This tab runs the model. <code>scripts/agent-server.mjs</code> gives it an OpenAI-shaped address on :8017.
|
| 46 |
+
<br><small>Alternative, and the one to prefer for real use: <code>npm run station</code> runs the engine in-process on
|
| 47 |
+
dawn.node (no browser tab, no throttling) and takes the same :8017. They cannot both hold the port β run one.</small></div>
|
| 48 |
+
|
| 49 |
+
<div class="card">
|
| 50 |
+
<div id="status">bootingβ¦</div>
|
| 51 |
+
<div class="sub" style="margin:.35rem 0 0" id="model"></div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="card">
|
| 55 |
+
<div style="margin-bottom:.5rem">Point a client at <code>http://127.0.0.1:8017/v1</code></div>
|
| 56 |
+
<div class="sub" style="margin:0">
|
| 57 |
+
<b>Cline</b> β API Provider βOpenAI Compatibleβ, Base URL above, any model id.<br>
|
| 58 |
+
<b>pi</b> β <code>~/.pi/agent/models.json</code>, <code>api: "openai-completions"</code>, that baseUrl.<br>
|
| 59 |
+
<b>curl</b> β <code>curl 127.0.0.1:8017/v1/chat/completions -d '{"messages":[{"role":"user","content":"hi"}]}' -H 'content-type: application/json'</code>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="card">
|
| 64 |
+
<div class="warn" style="margin-bottom:.5rem">Keep this tab visible.</div>
|
| 65 |
+
<div class="sub" style="margin:0">Chrome throttles background tabs hard β share.ts measured ~23 tok/s
|
| 66 |
+
backgrounded against ~65 focused. An agent run is long and unattended, which is exactly when a tab
|
| 67 |
+
stops being focused.</div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="card"><pre id="log"></pre></div>
|
| 71 |
+
|
| 72 |
+
<!-- loading-ui writes to progress-bar / -status / -detail / -log and reveals
|
| 73 |
+
this by adding .active. The bar was id="progress", there was no log sink,
|
| 74 |
+
and `hidden` is an attribute that .active cannot override β so a
|
| 75 |
+
multi-gigabyte download showed "bootingβ¦" and nothing else. Same
|
| 76 |
+
display:none + .active shape share.html already uses. -->
|
| 77 |
+
<div id="progress-wrap" class="card"><div id="progress-bar"></div><div id="progress-status"></div><div id="progress-detail"></div><pre id="progress-log"></pre></div>
|
| 78 |
+
<div id="badge" hidden></div>
|
assets/agent-host-eDUnNJBhJx.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js"])))=>i.map(i=>d[i]);
|
| 2 |
+
var te=Object.defineProperty;var ne=(e,t,n)=>t in e?te(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var P=(e,t,n)=>ne(e,typeof t!="symbol"?t+"":t,n);import{m as re}from"./model-registry-BoK-ObgACo.js";import{_ as oe}from"./preload-helper-ckwbz45pO3.js";import{bootEngine as ae}from"./loading-ui-CjCwSRuXvh.js";import{buildDecodeEngine as se,allocKVPages as ie}from"./engine-core-CLR3cxsqf4.js";import{parseVariantFlags as le}from"./variants-De-VVc9BRz.js";import{specFromSearch as ce,buildChatPromptFor as ue}from"./model-select-DhhV9mCi0T.js";import"./weight-loader-ByOLMd9f5C.js";const M=0xffffffffffffffffn,fe=0x243f6a8885a308d3n,me=0x13198a2e03707344n;function he(e){return e=(e^e>>30n)*0xbf58476d1ce4e5b9n&M,e=(e^e>>27n)*0x94d049bb133111ebn&M,(e^e>>31n)&M}function X(e,t){let n=e;for(let o=0;o<t.length;o++)n=he(n^BigInt(t[o]>>>0)+0x9e3779b97f4a7c15n&M);return n}function pe(e){const t=X(fe,e),n=X(me,e);return t.toString(16).padStart(16,"0")+n.toString(16).padStart(16,"0")}const de=new Set(["splitK"]),ge="zero-tvm/kv-prefix/1";function ye(e){const t=e.variants,n=Object.keys(t).filter(o=>!de.has(o)).sort().map(o=>` ${o}=${String(t[o])}`);return[ge,`spec=${e.specId}`,`weights=${e.weightRevision}`,`fused=${e.fused}`,`int8kv=${e.int8KV}`,"layers=all",`prefill=${e.prefillPath}`,`adapter=${e.adapter.vendor}|${e.adapter.architecture}`,"variants:",...n].join(`
|
| 3 |
+
`)}function we(e){const t=ye(e),n=new Array(t.length);for(let o=0;o<t.length;o++)n[o]=t.charCodeAt(o);return pe(n)}const be="zero-tvm-kvpool",F=2;function Y(e){const t={specId:e.spec.id,weightRevision:e.weightRevision,variants:e.variants,fused:e.fused,int8KV:e.int8KV,prefillPath:e.prefillPath,adapter:e.adapter};return we(t)}async function z(e,t){try{return await(await(await navigator.storage.getDirectory()).getDirectoryHandle(be,{create:t})).getDirectoryHandle(e,{create:t})}catch{return null}}async function $e(e,t){var f,s;const n=await e.exportKV();if(!n)return{tokens:0,reason:"engine state not poolable (MLA/int8/stage/invalid/mid-chunk)"};const o=await z(Y(t),!0);if(!o)return{tokens:0,reason:"OPFS unavailable"};const r={format:F,tokens:n.tokens,ids:n.ids,layerBytes:((f=n.layers[0])==null?void 0:f.byteLength)??0,scaleBytes:((s=n.scales[0])==null?void 0:s.byteLength)??0,layerCount:n.layers.length,gdnBytes:n.gdn.map(l=>l.byteLength),savedAt:Date.now()},a=await(await o.getFileHandle("entry.bin",{create:!0})).createWritable();for(const l of n.layers)await a.write(l);for(const l of n.scales)await a.write(l);for(const l of n.gdn)await a.write(l);await a.close();const i=await(await o.getFileHandle("meta.json",{create:!0})).createWritable();return await i.write(JSON.stringify(r)),await i.close(),{tokens:n.tokens}}async function xe(e,t,n){const o=await z(Y(t),!1);if(!o)return{restored:0,reason:"no entry for this fingerprint"};let r,a;try{r=JSON.parse(await(await(await o.getFileHandle("meta.json")).getFile()).text()),a=await(await(await o.getFileHandle("entry.bin")).getFile()).arrayBuffer()}catch{return{restored:0,reason:"no entry for this fingerprint"}}if(r.format!==F)return{restored:0,reason:`entry format ${r.format} != ${F}`};if(r.tokens>=n.length)return{restored:0,reason:"prompt does not extend the entry"};for(let u=0;u<r.tokens;u++)if(n[u]!==r.ids[u])return{restored:0,reason:`id mismatch at ${u}`};const i=(r.layerBytes+r.scaleBytes)*r.layerCount+r.gdnBytes.reduce((u,g)=>u+g,0);if(a.byteLength!==i)return{restored:0,reason:"entry truncated"};const f=[];let s=0;for(let u=0;u<r.layerCount;u++)f.push(a.slice(s,s+r.layerBytes)),s+=r.layerBytes;const l=[],h=[];if(r.scaleBytes>0)for(let u=0;u<r.layerCount;u++)h.push(a.slice(s,s+r.scaleBytes)),s+=r.scaleBytes;for(const u of r.gdnBytes)l.push(a.slice(s,s+u)),s+=u;return e.importKV({tokens:r.tokens,ids:r.ids,layers:f,scales:h,gdn:l})?{restored:r.tokens}:{restored:0,reason:"engine refused the snapshot"}}function _(e){if(e==null)return"null";if(Array.isArray(e))return`[${e.map(_).join(", ")}]`;if(typeof e=="object"){const t=e,n=[];for(const o of Object.keys(t))t[o]!==void 0&&n.push(`${JSON.stringify(o)}: ${_(t[o])}`);return`{${n.join(", ")}}`}return JSON.stringify(e)}const ke=`# Tools
|
| 4 |
+
|
| 5 |
+
You may call one or more functions to assist with the user query.
|
| 6 |
+
|
| 7 |
+
You are provided with function signatures within <tools></tools> XML tags:
|
| 8 |
+
<tools>`,ve=`
|
| 9 |
+
</tools>
|
| 10 |
+
|
| 11 |
+
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
|
| 12 |
+
<tool_call>
|
| 13 |
+
{"name": <function-name>, "arguments": <args-json-object>}
|
| 14 |
+
</tool_call>`,_e=`# Tools
|
| 15 |
+
|
| 16 |
+
You have access to the following functions:
|
| 17 |
+
|
| 18 |
+
<tools>`,Oe=`
|
| 19 |
+
</tools>
|
| 20 |
+
|
| 21 |
+
If you choose to call a function ONLY reply in the following format with NO suffix:
|
| 22 |
+
|
| 23 |
+
<tool_call>
|
| 24 |
+
<function=example_function_name>
|
| 25 |
+
<parameter=example_parameter_1>
|
| 26 |
+
value_1
|
| 27 |
+
</parameter>
|
| 28 |
+
<parameter=example_parameter_2>
|
| 29 |
+
This is the value for the second parameter
|
| 30 |
+
that can span
|
| 31 |
+
multiple lines
|
| 32 |
+
</parameter>
|
| 33 |
+
</function>
|
| 34 |
+
</tool_call>
|
| 35 |
+
|
| 36 |
+
<IMPORTANT>
|
| 37 |
+
Reminder:
|
| 38 |
+
- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags
|
| 39 |
+
- Required parameters MUST be specified
|
| 40 |
+
- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after
|
| 41 |
+
- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls
|
| 42 |
+
</IMPORTANT>`,Se=`Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.
|
| 43 |
+
|
| 44 |
+
Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables.
|
| 45 |
+
|
| 46 |
+
`;function Le(e,t,n){var s;if(n.length===0)return t.slice();const o=n.map(l=>`
|
| 47 |
+
${_(l)}`).join("");if(e==="llama3"){const l=t.slice(),h=l.findIndex(u=>u.role==="user");if(h<0)throw new Error("Cannot put tools in the first user message when there is no user message");const $=n.map(u=>`${JSON.stringify(u,null,4)}
|
| 48 |
+
|
| 49 |
+
`).join("");return l[h]={role:"user",content:Se+$+l[h].content.trim()},l}const r=e==="chatml-xml"?_e+o+Oe:ke+o+ve,a=t.slice(),i=((s=a[0])==null?void 0:s.role)==="system"?a[0].content:null;let f;if(e==="chatml-xml"){const l=(i??"").trim();f=l?`${r}
|
| 50 |
+
|
| 51 |
+
${l}`:r}else f=i?`${i}
|
| 52 |
+
|
| 53 |
+
${r}`:r;return i===null?a.unshift({role:"system",content:f}):a[0]={role:"system",content:f},a}function Ee(e,t,n){if(n.length===0)return t;if(e==="llama3"){if(n.length>1)throw new Error("llama3 renders a single tool call per assistant message");return`{"name": "${n[0].name}", "parameters": ${_(n[0].arguments)}}`}if(e==="chatml-xml"){let r=t.trim();return n.forEach((a,i)=>{r+=i===0?r?`
|
| 54 |
+
|
| 55 |
+
`:"":`
|
| 56 |
+
`,r+=`<tool_call>
|
| 57 |
+
<function=${a.name}>
|
| 58 |
+
`;for(const[f,s]of Object.entries(a.arguments))r+=`<parameter=${f}>
|
| 59 |
+
${typeof s=="string"?s:_(s)}
|
| 60 |
+
</parameter>
|
| 61 |
+
`;r+=`</function>
|
| 62 |
+
</tool_call>`}),r}let o=t;return n.forEach((r,a)=>{(a>0||t)&&(o+=`
|
| 63 |
+
`),o+=`<tool_call>
|
| 64 |
+
{"name": "${r.name}", "arguments": ${_(r.arguments)}}
|
| 65 |
+
</tool_call>`}),o}const B="<tool_call>",K="</tool_call>";function G(e,t){const n=e.indexOf("{",t);if(n<0)return null;let o=0,r=!1,a=!1;for(let i=n;i<e.length;i++){const f=e[i];if(r){a?a=!1:f==="\\"?a=!0:f==='"'&&(r=!1);continue}if(f==='"')r=!0;else if(f==="{")o++;else if(f==="}"&&--o===0)return{start:n,end:i+1}}return{start:n,end:-1}}function Te(e,t,n){const o=Math.min(n.length-1,e.length-t);for(let r=o;r>0;r--)if(e.endsWith(n.slice(0,r)))return r;return 0}function Q(e,t){if(typeof e!="object"||e===null||Array.isArray(e))return{name:"",arguments:{},raw:t,error:"tool call is not a JSON object"};const n=e;if(typeof n.name!="string"||n.name==="")return{name:"",arguments:{},raw:t,error:'tool call has no "name"'};let o=n.arguments??n.parameters;if(typeof o=="string")try{o=JSON.parse(o)}catch{}return o==null?{name:n.name,arguments:{},raw:t}:typeof o!="object"||Array.isArray(o)?{name:n.name,arguments:{},raw:t,error:"tool call arguments are not an object"}:{name:n.name,arguments:o,raw:t}}function Ce(e){const t=G(e,0);if(!t||t.end<0)return{name:"",arguments:{},raw:e,error:"no JSON object in <tool_call> block"};const n=e.slice(t.start,t.end);let o;try{o=JSON.parse(n)}catch(r){return{name:"",arguments:{},raw:e,error:`invalid JSON in <tool_call> block: ${r.message}`}}return Q(o,e)}function Ae(e){const t=e.indexOf("<function="),n=t<0?-1:e.indexOf(">",t);if(t<0||n<0)return{name:"",arguments:{},raw:e,error:"no <function=...> in <tool_call> block"};const o=e.slice(t+10,n),r=e.indexOf("</function>",n);if(r<0)return{name:o,arguments:{},raw:e,error:"unterminated <function=...> block"};const a={},i=e.slice(n+1,r);let f=0;for(;;){const s=i.indexOf("<parameter=",f);if(s<0)break;const l=i.indexOf(">",s);if(l<0)return{name:o,arguments:a,raw:e,error:"unterminated <parameter=...> tag"};let h=-1;for(let g=i.indexOf("</parameter>",l);g>=0;g=i.indexOf("</parameter>",g+1)){const C=i.slice(g+12).trim();if(C===""||C.startsWith("<parameter=")){h=g;break}}if(h<0)return{name:o,arguments:a,raw:e,error:"unterminated <parameter> value"};const $=i.slice(s+11,l);let u=i.slice(l+1,h);u.startsWith(`
|
| 66 |
+
`)&&(u=u.slice(1)),u.endsWith(`
|
| 67 |
+
`)&&(u=u.slice(0,-1)),a[$]=Ne(u),f=h+12}return{name:o,arguments:a,raw:e}}function Ne(e){try{const t=JSON.parse(e);return typeof t=="string"?e:t}catch{return e}}function Re(e,t){return e==="llama3"?Pe(t):Me(e,t)}function Me(e,t){const n=[];let o="",r=!1,a=0;for(;a<t.length;){const i=t.indexOf(B,a);if(i<0){const h=Te(t,a,B);o+=t.slice(a,t.length-h),r=h>=2;break}o+=t.slice(a,i);const f=i+B.length,s=t.indexOf(K,f);if(s<0){r=!0;break}const l=t.slice(f,s);n.push(e==="chatml-xml"?Ae(l):Ce(l)),a=s+K.length}return{text:o,calls:n,partial:r}}function Pe(e){const t=[];let n="",o=!1,r=0;for(;r<e.length;){const a=G(e,r);if(!a){n+=e.slice(r);break}let i=e.slice(r,a.start);if(i.endsWith("<|python_tag|>")&&(i=i.slice(0,-14)),a.end<0){n+=i,o=!0;break}const f=e.slice(a.start,a.end);let s=null;try{const l=Q(JSON.parse(f),f);l.error||(s=l)}catch{}n+=s?i:e.slice(r,a.end),s&&t.push(s),r=a.end}return{text:n,calls:t,partial:o}}const R=new URL(location.href).searchParams.get("server")??"http://127.0.0.1:8017",J=e=>document.getElementById(e),T=(e,t="")=>{const n=J("status");n.textContent=e,n.className=t},w=e=>{const t=J("log");t.textContent=`${new Date().toLocaleTimeString()} ${e}
|
| 68 |
+
${t.textContent}`.split(`
|
| 69 |
+
`).slice(0,200).join(`
|
| 70 |
+
`)},m=ce(location.search),U=re(m);J("model").textContent=`${U.name} β ${U.params}`;const Be=new Set(["qwen36","qwen3-8-27b"]);function Fe(e){return e==="llama3"?"llama3":[...Be].some(t=>m.id.startsWith(t))?"chatml-xml":"chatml-json"}async function Je(){const{isModelCached:e}=await oe(async()=>{const{isModelCached:n}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:n}},__vite__mapDeps([0,1,2]));if(await e(m))return;const t=document.createElement("dialog");t.id="agent-gate",t.style.cssText="width:calc(100vw - 2rem);max-width:32rem;background:#0f1216;color:#d6dbe1;border:1px solid #21262d;border-radius:10px;padding:1.5rem 1.6rem;font:inherit",t.innerHTML=`
|
| 71 |
+
<h2 style="font-size:1rem;margin:0 0 .7rem">Run ${m.id} in this tab?</h2>
|
| 72 |
+
<p style="color:#8b949e;margin:0 0 .7rem">
|
| 73 |
+
This is a developer surface. It downloads the model
|
| 74 |
+
and then talks to an agent-server on <code>127.0.0.1:8017</code> β
|
| 75 |
+
start it with <code>npm run agent</code> from the repo. Without that
|
| 76 |
+
server running, this page has nothing to serve.
|
| 77 |
+
</p>
|
| 78 |
+
<p class="warn" style="margin:0 0 1.1rem">Weights download once and cache locally.</p>
|
| 79 |
+
<div style="display:flex;gap:.8rem;justify-content:flex-end">
|
| 80 |
+
<a href="/" style="color:#7d8590">Not now</a>
|
| 81 |
+
<button type="button" id="agent-gate-go" autofocus
|
| 82 |
+
style="background:#d29922;color:#0b0d10;border:0;border-radius:8px;padding:.55rem 1.1rem;
|
| 83 |
+
font:inherit;font-weight:600;cursor:pointer">Download & boot</button>
|
| 84 |
+
</div>`,document.body.appendChild(t),t.showModal(),await new Promise(n=>{var o;(o=t.querySelector("#agent-gate-go"))==null||o.addEventListener("click",()=>{t.close(),t.remove(),n()},{once:!0})})}async function Ie(){let e=null;if(!("gpu"in navigator)){T("cannot boot: this browser has no WebGPU β Chrome and Edge ship it","err");return}await Je();const t=await ae({spec:m,optionalFeatures:["subgroups","chromium-experimental-subgroup-matrix"],probeSubgroups:!0,buildEngine:({device:c,weights:d,sgSizeOk:O,spec:S})=>(e=le(location.search,{hasSubgroupsFeature:c.features.has("subgroups"),sgSizeOk:O}),se(c,d,ie(c,S),{spec:S,variants:e}))});if(!t.ok)throw T(`cannot boot: ${t.reason}`,"err"),new Error(t.reason);const{engine:n,tokenizer:o}=t,r=Fe(m.chatTemplateId),a=new URL(location.href).searchParams.get("pool")!=="0",i=()=>({spec:m,weightRevision:m.hfRepo,variants:e,fused:!1,int8KV:!1,prefillPath:!m.moe&&e.subgroups?"chunked":"per-token",adapter:{vendor:"browser",architecture:"webgpu"}});let f=!1,s=null,l=null;const h=()=>{a&&(s!==null&&clearTimeout(s),s=self.setTimeout(()=>{s=null,!g&&(l=(async()=>{try{const c=await $e(n,i());c.tokens&&w(`pool: saved ${c.tokens} tokens`)}catch(c){w(`pool: save failed (${c.message})`)}})().finally(()=>{l=null}))},1500))};T(`ready β ${m.maxContext.toLocaleString()} token context, ${r}`,"ok"),w(`engine up: ${m.id}, dialect ${r}`);class ${constructor(d){P(this,"buf","");P(this,"timer",null);this.id=d}push(d){this.buf+=d,this.timer===null&&(this.timer=self.setTimeout(()=>{this.timer=null,this.flush()},60))}async flush(){if(this.timer!==null&&(clearTimeout(this.timer),this.timer=null),!this.buf)return;const d=this.buf;this.buf="",await u({type:"delta",id:this.id,text:d})}}async function u(c){try{await fetch(`${R}/agent/emit`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(c)})}catch(d){w(`emit failed: ${d.message}`)}}let g=!1;async function C(c){var S,I;if(g){await u({type:"error",id:c.id,message:"busy: this tab serves one request at a time"});return}g=!0,s!==null&&(clearTimeout(s),s=null),l&&await l;const d=performance.now(),O=new $(c.id);try{let x=c.messages.map(p=>{var E;if(p.role!=="assistant"||!((E=p.toolCalls)!=null&&E.length))return{role:p.role,content:p.content};const k=p.toolCalls.map(v=>{var j,q;let W={};try{W=JSON.parse(((j=v.function)==null?void 0:j.arguments)||"{}")}catch{}return{name:((q=v.function)==null?void 0:q.name)??"",arguments:W}});return{role:p.role,content:Ee(r,p.content??"",k)}});(S=c.tools)!=null&&S.length&&(x=Le(r,x,c.tools));const y=ue(m,x,o);if(a&&!f){f=!0;const p=await xe(n,i(),y);w(p.restored?`pool: restored ${p.restored} tokens`:`pool: miss (${p.reason})`)}if(y.length>=m.maxContext)throw new Error(`prompt is ${y.length} tokens, context is ${m.maxContext}. Lower the client's context window, or use a spec with a bigger one (qwen35 and llama32 are 32,768).`);const D=m.maxContext-y.length,H=Math.max(1,Math.min(c.maxTokens??D,D));n.setSampling(c.temperature!=null&&c.temperature>0?{temperature:c.temperature,topP:c.topP??1}:null);let b="";const A={stop:null},L=await n.generatePipelined(y,H,p=>{var E;const k=o.decode([p]);if(b+=k,O.push(k),(E=c.stop)!=null&&E.length){for(const v of c.stop)if(v&&b.endsWith(v)){A.stop=v;break}}},()=>A.stop!==null);await O.flush(),A.stop&&(b=b.slice(0,b.length-A.stop.length));const V=(I=c.tools)!=null&&I.length?Re(r,b):{text:b,calls:[]},N=V.calls.map((p,k)=>({id:`call_${c.id.slice(0,8)}_${k}`,type:"function",function:{name:p.name,arguments:JSON.stringify(p.arguments??{})}})),ee=(performance.now()-d)/1e3;w(`${c.id.slice(0,8)} ${y.length} in / ${L.length} out ${(L.length/ee).toFixed(1)} tok/s`+(N.length?` ${N.length} tool call(s)`:"")),await u({type:"done",id:c.id,text:V.text,toolCalls:N,finishReason:N.length?"tool_calls":L.length>=H?"length":"stop",usage:{prompt_tokens:y.length,completion_tokens:L.length,total_tokens:y.length+L.length}})}catch(x){await O.flush(),w(`${c.id.slice(0,8)} ERROR ${x.message}`),await u({type:"error",id:c.id,message:x.message})}finally{g=!1,h()}}function Z(){const c=new EventSource(`${R}/agent/jobs?model=${encodeURIComponent(m.id)}`);c.onopen=()=>{T(`hosting ${m.id} β ${R}/v1`,"ok"),w("connected to agent-server")},c.onmessage=d=>{C(JSON.parse(d.data))},c.onerror=()=>{T(`agent-server unreachable at ${R} β retrying`,"err")}}Z()}Ie();
|
assets/cache-probe-BYX_n9OPRB.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{p as s,a as l,o as f}from"./weight-loader-ByOLMd9f5C.js";import{o as c}from"./model-registry-BoK-ObgACo.js";async function g(t){return t.weightFormat!=="mlx-safetensors",!1}async function y(t){var n;if(typeof navigator>"u"||!((n=navigator.storage)!=null&&n.getDirectory))return!1;try{const o=await(await navigator.storage.getDirectory()).getDirectoryHandle(c(t));if(t.weightFormat==="mlx-safetensors"){const e=s(t),r=e[e.length-1];await o.getFileHandle(l(r.plan,r.layer))}else{const e=await o.getFileHandle(t.manifestName??"ndarray-cache.json"),r=JSON.parse(await(await e.getFile()).text()),i=[...new Set((r.records??[]).map(a=>a.dataPath).filter(a=>!!a))];if(i.length===0)return!1;for(const a of i)await o.getFileHandle(f(a))}return!0}catch{return await g(t)}}export{y as isModelCached};
|
assets/chat-flow-eTAzKAlIYe.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{buildChatPromptFor as B}from"./model-select-DhhV9mCi0T.js";import{allocKVPagesInt8 as ce,allocKVPages as de,buildDecodeEngine as ue}from"./engine-core-CLR3cxsqf4.js";import{parseVariantFlags as he}from"./variants-De-VVc9BRz.js";import{bootEngine as pe,setBadge as _,log as O}from"./loading-ui-CjCwSRuXvh.js";import{s as fe,q as me,a as Y,w as ge,b as ee,c as te,o as ke}from"./chat-ui-CQAxV1po6S.js";import{m as we}from"./model-registry-BoK-ObgACo.js";function be(t,o,u=3,l=3){let i=o,p=0,g=0,r=0;for(;i<t.length;){if(p++,i<u){i++;continue}const e=i-u;let c=-1;for(let a=e-1;a>=0;a--){if(a+u>e)continue;let m=!0;for(let d=0;d<u;d++)if(t[a+d]!==t[e+d]){m=!1;break}if(m){c=a;break}}if(c<0){i++;continue}r++;let s=0;for(let a=0;a<l;a++){const m=c+u+a,d=i+a;if(m>=t.length||d>=t.length||t[m]!==t[d])break;s++}g+=s,i+=1+s}return{totalSteps:p,totalAccepted:g,totalHits:r}}function ye(t,o,u,l=3,i=3){const{totalSteps:p,totalAccepted:g,totalHits:r}=be(o,u,l,i),e=p>0?g/p:0,c=i>0?e/i:0,s=2*(1+e)/(i+1),a=p>0?r/p:0;return{name:t,N:l,K:i,totalSteps:p,totalAccepted:g,meanAcceptedPerStep:e,acceptanceRate:c,theoreticalSpeedup:s,hitRate:a}}const xe="Section %d. The transport of water through a plant begins at the root hairs, where dissolved minerals enter by active transport and water follows by osmosis. Cohesion between water molecules and adhesion to the xylem walls maintain a continuous column that is pulled upward as vapour escapes from the stomata. The rate of that escape depends on humidity, temperature, and wind, so a plant that closes its stomata to conserve water also limits the carbon dioxide available to its chloroplasts. ";function q(t){let o="";for(let u=1;u<=t;u++)o+=xe.replace("%d",String(u));return o}function $e(t,o,u=4){const i=o(q(4)).length,p=o("").length,g=Math.max(1,(i-p)/4);let r=Math.max(0,Math.round((t-p)/g)),e=o(q(r));for(let a=0;a<200&&Math.abs(e.length-t)>u&&!(e.length>t&&r===0||(r+=e.length<t?1:-1,e=o(q(r)),r===0));a++);let c=q(r),s=c.split(" ");for(let a=0;a<400&&e.length>t+u&&s.length>1;a++)s=s.slice(0,-1),c=s.join(" "),e=o(c);for(let a=0;a<400&&e.length<t-u;a++)c+=" water",e=o(c);return{userMessage:c,actualTokens:e.length,blocks:r}}const Se=[64,256,1024,2048];function ve(t){const{engine:o,tokenizer:u}=t;window.bench=async(l=128,i=3,p=!1)=>{if(t.isBusy())return console.warn("[bench] decode already in flight β skipping"),null;t.setBusy(!0);try{const r=[{role:"system",content:"You are a helpful assistant."},{role:"user",content:"Write a four-sentence explanation of how photosynthesis works."}],e=B(o.spec,r,u);if(p){const f=await o.profileStep(e);return f?(console.log(`[profile] total = ${f.totalMs.toFixed(3)} ms/token across ${f.kernels.reduce((w,T)=>w+T.calls,0)} dispatches`),console.table(f.kernels.map(w=>({kernel:w.label,calls:w.calls,"ms/token":+w.totalMs.toFixed(3),"ms/call":+(w.totalMs/w.calls).toFixed(4),"% total":+w.pctOfTotal.toFixed(1)}))),f):(console.warn("[bench] profile requested but timestamp-query feature unavailable"),null)}const c=[];for(let f=0;f<=i;f++){const w=f===0?"warmup":`run${f}/${i}`;o.resetKVTracking();const T=performance.now();let $=0,P=0;await o.generatePipelined(e,l,()=>{$===0&&(P=performance.now()),$++});const C=performance.now(),A=(C-T)/1e3,R=$/A,D=P?P-T:A*1e3,z=$>1&&P?($-1)/((C-P)/1e3):R;console.log(`[bench] ${w}: ${$} tok / ${A.toFixed(2)}s = ${R.toFixed(2)} tok/s total Β· ttft ${D.toFixed(0)}ms Β· decode ${z.toFixed(2)} tok/s`),f>0&&c.push({tokens:$,seconds:A,tokPerS:R,ttftMs:D,decodeTokPerS:z})}const s=f=>f.slice().sort((w,T)=>w-T)[Math.floor(f.length/2)],a=c.map(f=>f.tokPerS).sort((f,w)=>f-w),m=a[Math.floor(a.length/2)],d=a.reduce((f,w)=>f+w,0)/a.length,b=a[0],S=a[a.length-1],k=s(c.map(f=>f.decodeTokPerS)),x=s(c.map(f=>f.ttftMs)),y={runs:c,median:m,mean:d,min:b,max:S,medianDecode:k,medianTtftMs:x};return console.log(`[bench] summary: median=${m.toFixed(2)} mean=${d.toFixed(2)} min=${b.toFixed(2)} max=${S.toFixed(2)} tok/s Β· decode=${k.toFixed(2)} tok/s Β· ttft=${x.toFixed(0)}ms`),y}finally{t.setBusy(!1),t.onIdle()}},window.benchTtft=async(l=Se,i=8,p=3)=>{if(t.isBusy())return console.warn("[ttft] decode already in flight β skipping"),null;t.setBusy(!0);try{const g=s=>s.slice().sort((a,m)=>a-m)[Math.floor(s.length/2)],r=s=>B(o.spec,[{role:"system",content:"You are a helpful assistant."},{role:"user",content:s}],u),e=[];for(const s of l){const a=$e(s,r),m=r(a.userMessage);if(m.length>o.maxContext){console.warn(`[ttft] target ${s}: ${m.length} tok exceeds maxContext ${o.maxContext} β skipped`);continue}const d=[],b=[];let S=0;o.resetKVTracking(),await o.generatePipelined(m,i,()=>{});for(let k=0;k<p;k++){o.resetKVTracking();const x=performance.now();let y=0,f=0;await o.generatePipelined(m,i,()=>{y===0&&(f=performance.now()),y++});const w=performance.now(),T=(w-x)/1e3,$=f?f-x:T*1e3,P=y>1&&f?(y-1)/((w-f)/1e3):y/T,C=o.getLastPrefill();C&&(S=C.chunks),d.push($),b.push(P),console.log(`[ttft] prompt=${m.length} tok (target ${s}) run${k+1}: ttft ${$.toFixed(0)}ms Β· ${y} gen tok Β· decode ${P.toFixed(2)} tok/s`+(C?` Β· prefill chunks ${C.chunks} (reused ${C.reused})`:""))}e.push({targetTokens:s,promptTokens:m.length,ttftMsRuns:d,ttftMs:g(d),decodeTokPerS:g(b),chunks:S})}console.log(""),console.log(" prompt tok | TTFT ms | ms/prompt-token | vs shortest | chunks"),console.log(" -----------+----------+------------------+--------------+-------");const c=e[0];for(const s of e)console.log(` ${String(s.promptTokens).padStart(10)} | ${s.ttftMs.toFixed(0).padStart(8)} | ${(s.ttftMs/s.promptTokens).toFixed(3).padStart(16)} | ${(s.ttftMs/c.ttftMs).toFixed(2).padStart(12)}x | ${String(s.chunks).padStart(6)}`);return{mode:"ttft-sweep",engine:"zero-tvm",runsPerPoint:p,genTokensPerPoint:i,points:e}}finally{t.setBusy(!1),t.onIdle()}},window.benchBatched=async(l=4,i=500,p="ffnDown")=>t.isBusy()?(console.warn("[batched-bench] decode in flight β skipping"),null):o.benchBatchedFfnDown(l,i,p),window.specSim=async(l=160,i=3,p=3)=>{if(t.isBusy())return console.warn("[specSim] decode in flight β skipping"),null;t.setBusy(!0);try{const g=[{name:"prose",messages:[{role:"system",content:"You are a helpful assistant."},{role:"user",content:"Write a four-sentence explanation of how photosynthesis works."}]},{name:"code",messages:[{role:"system",content:"You are a helpful coding assistant. Reply with only code, no prose."},{role:"user",content:"Write a TypeScript function that computes the nth Fibonacci number using memoization. Include a short test block that prints fib(10), fib(20), and fib(30)."}]},{name:"summary",messages:[{role:"system",content:"You are a helpful assistant."},{role:"user",content:"List the top five considerations when designing a REST API, with a one-sentence explanation for each."}]}],r=[];for(const e of g){const c=B(o.spec,e.messages,u),s=[];await o.generatePipelined(c,l,d=>{s.push(d)});const a=[...c,...s],m=ye(e.name,a,c.length,i,p);r.push({...m,generatedTokens:s.length}),console.log(`[specSim] ${e.name}: ${s.length} gen tok, hit=${(m.hitRate*100).toFixed(0)}%, Ξ±=${(m.acceptanceRate*100).toFixed(0)}%, accepted/step=${m.meanAcceptedPerStep.toFixed(2)}/${p}, theoretical speedup=${m.theoreticalSpeedup.toFixed(2)}Γ`)}return console.table(r.map(e=>({prompt:e.name,gen:e.generatedTokens,"hit %":+(e.hitRate*100).toFixed(1),"accept Ξ±":+(e.acceptanceRate*100).toFixed(1),"accepted/step":+e.meanAcceptedPerStep.toFixed(2),speedup:+e.theoreticalSpeedup.toFixed(2)}))),r}finally{t.setBusy(!1),t.onIdle()}},window.benchPrefill=async(l=800,i=3)=>{var p;if(t.isBusy())return console.warn("[benchPrefill] decode in flight β skipping"),null;t.setBusy(!0);try{const g="Photosynthesis is the process by which green plants convert light energy into chemical energy, storing it in the bonds of glucose molecules that fuel growth, respiration, and reproduction. ";let r=4,e=[];const c=d=>B(o.spec,[{role:"system",content:"You are a helpful assistant."},{role:"user",content:g.repeat(d)+`
|
| 2 |
+
Summarize the passage above in one sentence.`}],u);for(e=c(r);e.length<l;)r=Math.max(r+1,Math.ceil(r*l/e.length)),e=c(r);console.log(`[benchPrefill] prompt: ${e.length} tokens (target ${l})`);const s=[];for(let d=0;d<=i;d++){o.resetKVTracking();const b=performance.now();await o.generatePipelined(e,1,()=>{});const S=performance.now()-b,k=e.length/(S/1e3),x=d===0?"warmup":`run${d}/${i}`;console.log(`[benchPrefill] ${x}: ${S.toFixed(0)} ms to first token = ${k.toFixed(1)} prefill tok/s`),d>0&&s.push({ttftMs:S,tokPerS:k})}const a=s.map(d=>d.tokPerS).sort((d,b)=>d-b),m=a[Math.floor(a.length/2)];return console.log(`[benchPrefill] median: ${m.toFixed(1)} prefill tok/s (${((p=o.getLastPrefill())==null?void 0:p.chunks)??0} chunks/run)`),{promptTokens:e.length,runs:s,median:m,lastPrefill:o.getLastPrefill()}}finally{t.setBusy(!1),t.onIdle()}},window.benchTurns=async(l=120)=>{if(t.isBusy())return console.warn("[benchTurns] decode in flight β skipping"),null;t.setBusy(!0);try{const i="The city library reopened after a two-year renovation that added a glass atrium, a children's wing, quiet study pods, and a rooftop reading garden overlooking the river. ",p=[`Here is some background material: ${i.repeat(9)}
|
| 3 |
+
What changed in the renovation? Answer in detail.`,`More context: ${i.repeat(9)}
|
| 4 |
+
How do the study pods differ from the reading garden?`,"Now, in one short sentence: what overlooks the river?"];o.resetKVTracking();const g=[{role:"system",content:"You are a helpful, concise assistant."}],r=[];for(let e=0;e<p.length;e++){g.push({role:"user",content:p[e]});const c=B(o.spec,g,u),s=[],a=performance.now();let m=0;await o.generatePipelined(c,l,b=>{s.length===0&&(m=performance.now()-a),s.push(b)}),g.push({role:"assistant",content:u.decode(s)});const d=o.getLastPrefill();console.log(`[benchTurns] turn ${e+1}: prompt ${c.length} tok, reused ${(d==null?void 0:d.reused)??0}, chunks ${(d==null?void 0:d.chunks)??0}, TTFT ${m.toFixed(0)} ms, generated ${s.length}`),r.push({turn:e+1,promptTokens:c.length,ttftMs:m,...d})}return r}finally{t.setBusy(!1),t.onIdle()}},window.checkReuse=async()=>{var l;if(t.isBusy())return console.warn("[checkReuse] decode in flight β skipping"),null;t.setBusy(!0);try{o.resetKVTracking();const i=[{role:"system",content:"You are a helpful, concise assistant."},{role:"user",content:"Name three primary colors."}],p=B(o.spec,i,u),g=[];await o.generatePipelined(p,48,a=>g.push(a)),i.push({role:"assistant",content:u.decode(g)}),i.push({role:"user",content:"Which of those is the warmest?"});const r=B(o.spec,i,u),e=await o.debugCompareReuse(r),c=(((l=o.getLastPrefill())==null?void 0:l.chunks)??0)>0,s=e.maxAbsDiff===0?" (bit-exact β)":c?" (nonzero: prefix built by CHUNKED prefill vs per-token replay β kernel-variant numerics; rerun with ?chunk=0 for the bit-exact assertion)":" (MISMATCH β reuse rules broken!)";return console.log(`[checkReuse] prompt ${e.promptLen} tok, reused prefix ${e.startPos} β logits max|Ξ| = ${e.maxAbsDiff} mean|Ξ| = ${e.meanAbsDiff}`+s),{...e,chunkedPrefix:c}}finally{t.setBusy(!1),t.onIdle()}},console.log("[bench] harness ready β call `await bench(128, 3)` or `await bench(0, 0, true)` for per-kernel profile"),console.log("[bench] prefill: `await benchPrefill(800, 3)` Β· multi-turn TTFT: `await benchTurns()` Β· reuse assertion: `await checkReuse()`"),console.log("[bench] batched primitive: `await benchBatched(4, 500)` β ffnDown weight-reuse falsifiability test"),console.log("[bench] PLD acceptance sim: `await specSim(160, 3, 3)` β measures spec-decode upside without GPU changes")}const F=t=>document.getElementById(t);function ne(t){const o=F("stats");o&&(o.textContent=t)}function Le(t,o){const u=F("loading-error");if(u){if(u.textContent=t,o){const l=document.createElement("button");l.type="button",l.id="retry-download-btn",l.textContent="Retry download",l.style.cssText="display:block;margin-top:0.6rem;padding:0.45rem 1.1rem;background:var(--accent);color:#000;font-weight:600;font-size:0.8rem;border:none;border-radius:6px;cursor:pointer",l.addEventListener("click",()=>{u.classList.remove("visible"),u.replaceChildren(),o()},{once:!0}),u.appendChild(l)}u.classList.add("visible")}}function Ee(t){const{spec:o}=t,u=t.search??location.search,l=t.poolSlots??0;return pe({spec:o,...l?{expertPool:l}:{},optionalFeatures:["subgroups","timestamp-query","chromium-experimental-subgroup-matrix"],probeSubgroups:!0,onDeviceLost:t.onDeviceLost,buildEngine:({device:i,weights:p,sgSizeOk:g,spec:r})=>{const e=he(u,{hasSubgroupsFeature:i.features.has("subgroups"),sgSizeOk:g}),c=!r.qkNorm&&r.weightFormat!=="mlx-safetensors",s=e.int8KV&&!r.mla;e.int8KV&&!s&&O(`?kv8=1 ignored for ${r.id} β int8 KV does not cover MLA, which caches a latent rather than per-head K/V`);const a=Math.round(r.maxContext*r.kvBytesPerToken/(1024*1024)/(s?2:1));O(`Allocating ${s?"int8 ":""}KV cache β ${r.maxPages} pages Γ ${r.pageSize} = ${r.maxContext} tokens Β· ~${a} MiB`);const m=s?ce(i,r):de(i,r);O("Building decode engineβ¦");const d=new URLSearchParams(u),b=(k,x)=>{const y=d.get(k);if(y===null)return x;const f=Number(y);return Number.isFinite(f)?f:x},S=b("temp",0);return S>0&&O(`Sampling: temperature ${S}, top-p ${b("topp",1)}, min-p ${b("minp",0)}`),ue(i,p,m,{variants:e,fused:c,int8KV:s,spec:r,...l?{expertPool:l}:{},prefixReuse:d.get("reuse")!=="0",chunkedPrefill:d.get("chunk")!=="0",pooledChunkedPrefill:d.get("chunk")==="1",sampling:S>0?{temperature:S,topP:b("topp",1),minP:b("minp",0),seed:b("seed",0)}:void 0})},warmup:async(i,p,g)=>{const r=B(o,[{role:"user",content:"Hi."}],p),e=o.moe?r.slice(0,1):r,c=performance.now();g==null||g(`Warming up pipeline β ${e.length} token(s)${o.moe?" (single-pass: MoE)":""}β¦`),await i.generatePipelined(e,1,()=>{}),g==null||g(`Warmup done in ${((performance.now()-c)/1e3).toFixed(1)}s`)}})}function Ie(t){var J;const{spec:o,tokenizer:u,engine:l}=t,i=we(o),p=t.log??O,g=t.onPhase??(()=>{});fe(i.name,me(o));const r=F("composer"),e=F("inp"),c=F("btn"),s=F("stop-btn"),a=F("new-chat-btn"),m=F("suggest-grid"),d=F("ctx-hint"),b=F("welcome");if(!r||!e||!c||!s){console.error("[zero-tvm] chat UI elements missing");return}const S="You are a helpful, concise assistant. Use Markdown (numbered lists, **bold**, and fenced ```code``` blocks with a language tag) when it clarifies the answer.\n\nYou are running inside zero-tvm (zerotvm.com): an LLM inference engine written by hand in WGSL and TypeScript, running entirely in this browser tab on the user's own GPU through WebGPU β no server, no WebLLM, no TVM. Model weights download once from Hugging Face and cache on this device; prompts, generated tokens and the KV cache never leave the machine. The whole forward pass is 10 hand-written WGSL kernel roles, readable end-to-end. If asked what zero-tvm is, answer from this paragraph.",k=[{role:"system",content:S}];let x=!1,y=!1;const f=`zt-chat-${o.id}`;function w(){try{const n=k.filter(h=>h.role!=="system").slice(-40);localStorage.setItem(f,JSON.stringify(n))}catch{}}function T(){let n=[];try{n=JSON.parse(localStorage.getItem(f)??"[]")}catch{return}!Array.isArray(n)||n.length===0||(n.forEach((h,v)=>{var L;if(h.role==="user"&&typeof h.content=="string")k.push({role:"user",content:h.content}),ee(h.content);else if(h.role==="assistant"&&typeof h.content=="string"){const M={role:"assistant",content:h.content,ids:Array.isArray(h.ids)?h.ids:void 0};k.push(M),te().finish({fullText:h.content,tokens:((L=M.ids)==null?void 0:L.length)??0,tokPerS:0,...v===n.length-1?{onRegenerate:()=>{j()}}:{}})}}),C(B(o,k,u).length))}function $(){!e||!c||(c.disabled=x||e.value.trim().length===0)}function P(n){!e||!c||!s||(x=n,c.hidden=n,s.hidden=!n,s.disabled=!n,e.disabled=n,a&&(a.disabled=n),e.placeholder=n?"Generatingβ¦":`Ask ${i.name} anythingβ¦`)}function C(n){if(d)if(!n)d.textContent="Zero TVM Β· 10 WGSL kernel roles Β· β/β for commands";else if(n>=l.maxContext)d.textContent=`Context full β ${l.maxContext} / ${l.maxContext} tokens Β· start a new chat`;else{const h=Math.round(n/l.maxContext*100);d.textContent=`Context ${n} / ${l.maxContext} tokens (${h}%)`}}function A(){if(x)return;k.length=0,k.push({role:"system",content:S});try{localStorage.removeItem(f)}catch{}const n=F("messages");n&&n.replaceChildren(),b==null||b.classList.remove("hidden"),ne(""),_("Ready","ready"),C(),matchMedia("(pointer: coarse)").matches||e==null||e.focus()}e.disabled=!1,e.placeholder=`Ask ${i.name} anythingβ¦`,a&&(a.disabled=!1),Y(e),$(),C(),ge(),T();const R=!matchMedia("(pointer: coarse)").matches;R&&e.focus(),(J=t.lock)==null||J.onChange(n=>{x||(e.disabled=n,c.disabled=n||e.value.trim().length===0,e.placeholder=n?"Serving a room guestβ¦":`Ask ${i.name} anythingβ¦`)}),e.addEventListener("input",()=>{Y(e),$()}),e.addEventListener("keydown",n=>{n.key==="Enter"&&!n.shiftKey&&(n.preventDefault(),c.disabled||r.requestSubmit())}),r.addEventListener("submit",n=>{n.preventDefault(),c.disabled||se()}),s.addEventListener("click",()=>{y=!0,s.disabled=!0}),a==null||a.addEventListener("click",A),m==null||m.addEventListener("click",n=>{const h=n.target.closest(".suggest");if(!h)return;const v=h.dataset.prompt;v&&(e.value=v,Y(e),$(),r.requestSubmit())});async function D(n,h,v,L){const M=l.maxContext-h.length;p(`Prompt: ${h.length} tokens Β· reply budget ${M} tokens`),C(h.length);const E=v.length>0;E||n.showThinking(),g(E?"generating":"thinking");const K=performance.now();let I=0,V=!0;const H=v.slice();let W=E?u.decode(H):"";try{await l.generatePipelined(h,M,N=>{var X;(X=t.onToken)==null||X.call(t),V&&(E||n.body.replaceChildren(n.cursor),V=!1,g("generating")),I++,H.push(N),W=u.decode(H),n.render(W);const le=(performance.now()-K)/1e3;ne(`${I} tok Β· ${(I/le).toFixed(1)} tok/s`)},()=>y)}catch(N){W+=`
|
| 5 |
+
|
| 6 |
+
_[Error: ${N}]_`,p(`Error: ${N}`)}const re=(performance.now()-K)/1e3,ae=I/Math.max(re,.001),Z=h.length+I,Q=I>=M,ie=y&&!Q;return n.finish({fullText:W,tokens:H.length,tokPerS:ae,onRegenerate:()=>{j()},notice:Q?{text:`Cut off β the ${l.maxContext}-token context window is full. Start a new chat to keep going.`}:ie?{text:"Stopped β this reply is incomplete.",onContinue:L}:void 0}),g("idle"),C(Z),Z>=l.maxContext&&(_("Context full","error"),p(`Context window full (${l.maxContext} tokens) β start a new chat to continue.`)),{text:W,ids:H}}async function z(){var M,E,K,I;P(!0),y=!1,await((M=t.lock)==null?void 0:M.acquire()),(E=F("messages"))==null||E.querySelectorAll(".truncation-btn").forEach(V=>V.remove());const n=te(),h=B(o,k,u);if(h.length>=l.maxContext){const V=`Context full β the conversation (${h.length} tokens) exceeds the ${l.maxContext}-token window. Start a new chat.`;n.finish({fullText:`_${V}_`,tokens:0,tokPerS:0}),_("Context full","error"),C(h.length),p(V),(K=t.lock)==null||K.release(),P(!1),y=!1,$();return}const v={role:"assistant",content:""};k.push(v);const L=await D(n,h,[],()=>{G(v,n)});v.content=L.text,v.ids=L.ids,w(),(I=t.lock)==null||I.release(),P(!1),y=!1,$(),R&&(e==null||e.focus())}async function G(n,h){var M,E;if(x||!n.ids||k[k.length-1]!==n)return;P(!0),y=!1,await((M=t.lock)==null?void 0:M.acquire());const v=B(o,k.slice(0,-1),u).concat(n.ids),L=await D(h,v,n.ids,()=>{G(n,h)});n.content=L.text,n.ids=L.ids,w(),(E=t.lock)==null||E.release(),P(!1),y=!1,$(),R&&(e==null||e.focus())}const U={new:()=>A(),canvas:()=>{ke()||oe("No runnable code block yet β ask for html, svg, or js.")},...t.commands};function oe(n){var v;const h=document.createElement("div");h.className="sys-note",h.textContent=n,(v=F("messages"))==null||v.appendChild(h)}async function se(){if(x||!e)return;const n=e.value.trim();if(!n)return;const h=n.startsWith("/")?n.slice(1).split(/\s/)[0].toLowerCase():"";if(h&&U[h]){e.value="",Y(e),$(),U[h]();return}e.value="",Y(e),ee(n),k.push({role:"user",content:n}),w(),await z()}async function j(){var h;if(x||k.length===0||k[k.length-1].role!=="assistant")return;k.pop(),w();const n=(h=F("messages"))==null?void 0:h.querySelectorAll(".msg.ai");n&&n.length>0&&n[n.length-1].remove(),await z()}ve({engine:l,tokenizer:u,isBusy:()=>x,setBusy:P,onIdle:$})}export{Ee as b,Le as s,Ie as w};
|
assets/chat-ui-CQAxV1po6S.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{_}from"./preload-helper-ckwbz45pO3.js";const x='<svg class="icon" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>',H='<svg class="icon" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>',q='<svg class="icon" viewBox="0 0 24 24"><path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>';function M(t,s){t.innerHTML=`${x}<span>Copy</span>`,t.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(s()),t.classList.add("copied"),t.innerHTML=`${H}<span>Copied</span>`,setTimeout(()=>{t.classList.remove("copied"),t.innerHTML=`${x}<span>Copy</span>`},1500)}catch{}})}function u(t,s){const n=/(```)|(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\n]+\*)|\[([^\]]+)\]\(([^)\s]+)\)/g;let e=0,c;for(;(c=n.exec(t))!==null;)if(!c[1]){if(c.index>e&&s.appendChild(document.createTextNode(t.slice(e,c.index))),c[2]){const o=document.createElement("code");o.textContent=c[2].slice(1,-1),s.appendChild(o)}else if(c[3]){const o=document.createElement("strong");o.textContent=c[3].slice(2,-2),s.appendChild(o)}else if(c[4]){const o=document.createElement("em");o.textContent=c[4].slice(1,-1),s.appendChild(o)}else if(c[5]&&c[6])if(/^https?:\/\//i.test(c[6])){const o=document.createElement("a");o.href=c[6],o.textContent=c[5],o.target="_blank",o.rel="noopener noreferrer",s.appendChild(o)}else s.appendChild(document.createTextNode(c[0]));e=c.index+c[0].length}e<t.length&&s.appendChild(document.createTextNode(t.slice(e)))}const S='<svg class="icon" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4"/></svg>',B=new Set(["html","svg","js","javascript"]);let C=null;function F(){return C?(T(C.code,C.lang),!0):!1}function T(t,s){var r;_(()=>import("./feats-dlBfBxqpI8.js"),[]).then(a=>a.recordFeat("canvas")).catch(()=>{}),(r=document.getElementById("code-canvas"))==null||r.remove();const n=document.createElement("div");n.id="code-canvas",n.innerHTML=`
|
| 2 |
+
<div class="canvas-frame" role="dialog" aria-modal="true" aria-label="Code preview">
|
| 3 |
+
<div class="canvas-head">
|
| 4 |
+
<span class="canvas-title">canvas Β· ${s}</span>
|
| 5 |
+
<button type="button" class="canvas-close" aria-label="Close preview">β close</button>
|
| 6 |
+
</div>
|
| 7 |
+
<iframe class="canvas-view" sandbox="allow-scripts" title="Code preview"></iframe>
|
| 8 |
+
</div>`;const e=document.activeElement,c=()=>{var a;n.remove(),window.removeEventListener("keydown",o),(a=e==null?void 0:e.focus)==null||a.call(e)},o=a=>{a.key==="Escape"&&c()};n.addEventListener("click",a=>{const i=a.target;(i===n||i.closest(".canvas-close"))&&c()}),window.addEventListener("keydown",o);const p=t.replace(/<\/script/gi,"<\\/script"),l=s==="html"?t:s==="svg"?`<!doctype html><body style="margin:0;display:grid;place-items:center;min-height:100vh;background:#fff">${t}`:`<!doctype html><body style="margin:0;padding:14px;background:#fff;color:#111;font:13px/1.5 ui-monospace,monospace"><pre id="out"></pre><script>
|
| 9 |
+
var out=document.getElementById('out');
|
| 10 |
+
var log=function(){out.textContent+=Array.prototype.map.call(arguments,function(x){return typeof x==='object'?JSON.stringify(x):String(x)}).join(' ')+'\\n'};
|
| 11 |
+
console.log=console.info=console.warn=console.error=log;
|
| 12 |
+
window.addEventListener('error',function(e){log('Error:',e.message)});
|
| 13 |
+
<\/script><script>${p}<\/script>`;n.querySelector(".canvas-view").srcdoc=l,document.body.appendChild(n),n.querySelector(".canvas-close").focus()}function I(t,s){const n=document.createElement("div");n.className="code-block";const e=document.createElement("div");e.className="code-head";const c=document.createElement("span");c.className="code-lang",c.textContent=s||"code",e.appendChild(c);const o=(s||"").toLowerCase();if(B.has(o)){C={code:t,lang:o};const a=document.createElement("button");a.type="button",a.className="code-run",a.innerHTML=`${S}<span>Canvas</span>`,a.addEventListener("click",()=>T(t,o)),e.appendChild(a)}const p=document.createElement("button");p.type="button",p.className="code-copy",M(p,()=>t),e.appendChild(p),n.appendChild(e);const l=document.createElement("pre"),r=document.createElement("code");return s&&(r.className=`lang-${s}`),r.textContent=t,l.appendChild(r),n.appendChild(l),n}function L(t){const s=document.createDocumentFragment(),n=t.split(`
|
| 14 |
+
`);let e=0;for(;e<n.length;){const c=n[e];if(c.trim()===""){e++;continue}const o=/^\s*```(.*)$/.exec(c);if(o){const a=o[1].trim();e++;const i=[];for(;e<n.length&&!/^\s*```/.test(n[e]);)i.push(n[e]),e++;e<n.length&&e++,s.appendChild(I(i.join(`
|
| 15 |
+
`),a));continue}if(/^\s*(---|\*\*\*|___)\s*$/.test(c)){s.appendChild(document.createElement("hr")),e++;continue}const p=/^(#{1,6})\s+(.*)$/.exec(c);if(p){const a=Math.min(6,p[1].length),i=document.createElement(`h${a}`);u(p[2].trim(),i),s.appendChild(i),e++;continue}if(/^\s*>\s?/.test(c)){const a=document.createElement("blockquote"),i=[];for(;e<n.length&&/^\s*>\s?/.test(n[e]);)i.push(n[e].replace(/^\s*>\s?/,"")),e++;u(i.join(" "),a),s.appendChild(a);continue}if(/^\s*[-*]\s+/.test(c)){const a=document.createElement("ul");for(;e<n.length&&/^\s*[-*]\s+/.test(n[e]);){const i=document.createElement("li");u(n[e].replace(/^\s*[-*]\s+/,""),i),a.appendChild(i),e++}s.appendChild(a);continue}if(/^\s*\d+\.\s+/.test(c)){const a=document.createElement("ol");for(;e<n.length&&/^\s*\d+\.\s+/.test(n[e]);){const i=document.createElement("li");u(n[e].replace(/^\s*\d+\.\s+/,""),i),a.appendChild(i),e++}s.appendChild(a);continue}const l=e+1<n.length?n[e+1]:"";if(c.trim()!==""&&/^\s*(=+|-{2,})\s*$/.test(l)){const a=document.createElement(l.includes("=")?"h1":"h2");u(c.trim(),a),s.appendChild(a),e+=2;continue}const r=[];for(;e<n.length&&n[e].trim()!==""&&!/^\s*```/.test(n[e])&&!/^#{1,6}\s+/.test(n[e])&&!/^\s*>\s?/.test(n[e])&&!/^\s*[-*]\s+/.test(n[e])&&!/^\s*\d+\.\s+/.test(n[e])&&!/^\s*(=+|-{2,})\s*$/.test(n[e+1]??"");)r.push(n[e]),e++;if(r.length>0){const a=document.createElement("p");u(r.join(" "),a),s.appendChild(a)}}return s}const h=t=>document.getElementById(t);let b={name:"β¦",tag:""};function j(t,s){b={name:t,tag:s}}function O(t){var s;return t.weightFormat==="mlx-safetensors"?((s=t.moe)==null?void 0:s.bits)===3?"MLX 3-bit experts":"MLX 4-bit":"q4f16_1"}function k(){const t=h("welcome");t&&!t.classList.contains("hidden")&&t.classList.add("hidden")}function R(t){t.style.height="auto",t.style.height=Math.min(t.scrollHeight,200)+"px"}function E(t=!1){const s=h("chat-main");if(!s)return;const n=s.scrollHeight-s.scrollTop-s.clientHeight;(t||n<120)&&(s.scrollTop=s.scrollHeight)}let N=!1;function P(){if(N)return;const t=h("chat-main"),s=h("scroll-fab");if(!t||!s)return;N=!0;const n=()=>{const e=t.scrollHeight-t.scrollTop-t.clientHeight;s.classList.toggle("visible",e>140)};t.addEventListener("scroll",n,{passive:!0}),s.addEventListener("click",()=>E(!0)),n()}function D(t){var e;k();const s=document.createElement("div");s.className="msg user";const n=document.createElement("div");n.className="bubble",n.textContent=t,s.appendChild(n),(e=h("messages"))==null||e.appendChild(s),E(!0)}function V(){var p;k();const t=document.createElement("div");t.className="msg ai",t.innerHTML=`
|
| 16 |
+
<div class="role">
|
| 17 |
+
<span class="role-dot">Z</span>
|
| 18 |
+
<span class="role-name"></span>
|
| 19 |
+
<span class="model-tag"></span>
|
| 20 |
+
</div>
|
| 21 |
+
<div class="body"></div>
|
| 22 |
+
<div class="actions"></div>
|
| 23 |
+
`,t.querySelector(".role-name").textContent=b.name,t.querySelector(".model-tag").textContent=b.tag;const s=t.querySelector(".body"),n=document.createElement("span");n.className="cursor",s.appendChild(n),(p=h("messages"))==null||p.appendChild(t),E(!0);let e=!1,c="";const o=()=>{e=!1;const l=L(c);s.replaceChildren(l,n),E(!1)};return{wrap:t,body:s,cursor:n,showThinking(){s.replaceChildren();const l=document.createElement("span");l.className="thinking",l.innerHTML="<span></span><span></span><span></span>",s.appendChild(l)},render(l){c=l,e||(e=!0,requestAnimationFrame(o))},finish({fullText:l,tokens:r,tokPerS:a,onRegenerate:i,notice:g}){var y;const $=L(l);s.replaceChildren($),t.classList.add("msg-done"),(y=t.querySelector(".truncation"))==null||y.remove();const m=t.querySelector(".actions");if(!m)return;m.replaceChildren();const v=document.createElement("button");if(v.type="button",v.className="action-btn",M(v,()=>l),m.appendChild(v),i){const d=document.createElement("button");d.type="button",d.className="action-btn",d.innerHTML=`${q}<span>Regenerate</span>`,d.addEventListener("click",i),m.appendChild(d)}if(r>0){const d=document.createElement("span");d.className="msg-stats",d.textContent=a>0?`${r} tok Β· ${a.toFixed(1)} tok/s`:`${r} tok`,m.appendChild(d)}if(g){const d=document.createElement("div");d.className="truncation";const w=document.createElement("span");if(w.textContent=g.text,d.appendChild(w),g.onContinue){const f=document.createElement("button");f.type="button",f.className="truncation-btn",f.textContent="Continue",f.addEventListener("click",g.onContinue),d.appendChild(f)}t.insertBefore(d,m)}}}}export{R as a,D as b,V as c,F as o,O as q,j as s,P as w};
|
assets/engine-core-CLR3cxsqf4.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/feats-dlBfBxqpI8.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
const o=[{id:"first-summon",name:"First Summoning",desc:"Boot a model in the entrance"},{id:"lane-dense",name:"Dense Communion",desc:"Speak with a dense-attention model"},{id:"lane-hybrid",name:"Delta Communion",desc:"Speak with a recurrent hybrid"},{id:"lane-moe",name:"Woke the Swarm",desc:"Speak with a sparse MoE"},{id:"heavy",name:"Heavyweight",desc:"Boot a β₯10 GB footprint"},{id:"pooled",name:"Lean Build",desc:"Boot an expert-pool memory build"},{id:"long-ctx",name:"Long Sight",desc:"Boot a long-context build"},{id:"canvas",name:"Made It Real",desc:"Run model-written code in the canvas"},{id:"room",name:"Opened the Gates",desc:"Host a room for other machines"}],n="zt-feats";function a(){try{const e=JSON.parse(localStorage.getItem(n)??"[]");return new Set(Array.isArray(e)?e.filter(t=>typeof t=="string"):[])}catch{return new Set}}function r(e){try{const t=a();if(t.has(e))return;t.add(e),localStorage.setItem(n,JSON.stringify([...t])),document.dispatchEvent(new CustomEvent("zt-feat",{detail:e}))}catch{}}export{o as FEATS,a as feats,r as recordFeat};
|
assets/index-BqNbylMY24.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js","assets/landing-chat-Bo__3GxplQ.js","assets/preload-helper-ckwbz45pO3.js","assets/mascot-BCKWddkWrV.js","assets/chat-flow-eTAzKAlIYe.js","assets/model-select-DhhV9mCi0T.js","assets/engine-core-CLR3cxsqf4.js","assets/variants-De-VVc9BRz.js","assets/loading-ui-CjCwSRuXvh.js","assets/chat-ui-CQAxV1po6S.js","assets/feats-dlBfBxqpI8.js"])))=>i.map(i=>d[i]);
|
| 2 |
+
import{S as ie,m as I,s as he}from"./model-registry-BoK-ObgACo.js";import{_ as ce}from"./preload-helper-ckwbz45pO3.js";import{m as se,a as ae}from"./mascot-BCKWddkWrV.js";import{feats as pe,FEATS as ve}from"./feats-dlBfBxqpI8.js";const z={moe:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3 3-3 3-3-3zM5 9l2.4 2.4L5 13.8 2.6 11.4zM19 9l2.4 2.4L19 13.8l-2.4-2.4zM12 16l3 3-3 3-3-3z" opacity="0.9"/><circle cx="12" cy="11.4" r="1.6"/></svg>',hybrid:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 9c3 0 3-4 6-4s3 4 6 4 3-4 6-4"/><path d="M2 16h6l3-5 3 8 2-3h6" opacity="0.85"/></svg>',dense:'<svg viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1" opacity="0.55"/><rect x="4" y="13" width="7" height="7" rx="1" opacity="0.55"/><rect x="13" y="13" width="7" height="7" rx="1"/></svg>',mla:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 9-8 9-8-9z"/><path d="M12 8l4 4-4 4-4-4z" fill="currentColor" stroke="none" opacity="0.7"/></svg>',embed:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 12c2.5-4 6-6 9-6s6.5 2 9 6c-2.5 4-6 6-9 6s-6.5-2-9-6z"/><path d="M8 12h8" opacity="0.8"/></svg>'};function be(t){const s=[],o=t.layerKinds.filter(u=>u==="gdn").length;return t.moe&&(s.push({name:"Sparse Routing",desc:`${t.moe.topK} of ${t.moe.experts} experts wake per token`}),t.sharedExpertIndex>=0&&s.push({name:"Constant Companion",desc:"one shared expert always answers"})),o>0&&s.push({name:"Delta Rule",desc:`${o} recurrent layers β memory that does not grow`}),t.mla&&s.push({name:"Latent Gaze",desc:"attends through a compressed latent cache"}),!t.moe&&o===0&&!t.mla&&!t.embeddingOnly&&s.push({name:"Full Attention",desc:`every token sees every token, ${t.layers} layers deep`}),t.qkNorm&&s.push({name:"Steady Gaze",desc:"QK-norm holds attention in range"}),t.maxSeq>=131072&&s.push({name:"Long Sight",desc:`trained to a ${Math.round(t.maxSeq/1024)}k window`}),s.slice(0,3)}function H(t){return t.embeddingOnly?"embed":t.mla?"mla":t.moe?"moe":t.layerKinds.some(s=>s==="gdn")?"hybrid":"dense"}function ge(t){if(t.embeddingOnly)return"Returns a vector. It does not speak.";if(t.mla)return"Attends through a compressed latent β the cache is 7Γ smaller than it looks.";if(t.moe){const o=t.layerKinds.filter(u=>u==="gdn").length;return`Routes every token through ${t.moe.topK} of ${t.moe.experts} experts`+(o>0?`, ${o} of ${t.layers} layers recurrent.`:".")}const s=t.layerKinds.filter(o=>o==="gdn").length;return s>0?`${s} recurrent layers, ${t.layers-s} attention β memory that does not grow.`:`${t.layers} layers straight through, ${t.heads} heads each.`}const P=t=>t>=1024?`${Math.round(t/1024)}k`:String(t),F=new URLSearchParams(location.search).get("kv8")!=="0",le=(t,s)=>s&&!t.mla?t.kvBytesPerToken/2+2*t.kvHeads*2:t.kvBytesPerToken,ne=(t,s,o)=>{const u=s*le(t,o);return u>=2**30?`${(u/2**30).toFixed(1)} GB KV`:`${Math.round(u/2**20)} MB KV`};function G(t){const s=[{name:"Standard",tokens:t.maxContext}],o=Math.min(t.maxContext*4,t.maxSeq);return o>t.maxContext&&s.push({name:"Long",tokens:o}),t.maxSeq>o&&s.push({name:"Full",tokens:t.maxSeq}),s}function fe(t){const s=t.indexOf(" Β· ");return s<0?{family:t,variant:""}:{family:t.slice(0,s),variant:t.slice(s+3)}}function ye(){const t=new Map;for(const{param:s,spec:o}of ie){if(o.embeddingOnly)continue;const u=I(o);if(u.pending)continue;const{family:M,variant:y}=fe(u.params);let b=t.get(u.name);b||(b={name:u.name,params:M,variants:[]},t.set(u.name,b)),b.variants.push({param:s,spec:o,label:y||M})}return[...t.values()]}const f=ye(),re={Weights:'<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l3 3-3 3-3-3z"/><path d="M4 8l3 3-3 3-3-3z" opacity="0.6"/><path d="M12 8l3 3-3 3-3-3z" opacity="0.6"/></svg>',Context:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3 2h8l2 2v10H3z"/><path d="M5 6h6M5 9h6M5 12h4" opacity="0.7"/></svg>',Speed:'<svg viewBox="0 0 16 16" fill="currentColor"><path d="M9 1L3 9h4l-1 6 6-8H8z"/></svg>',Footprint:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M2 11h12v3H2z"/><path d="M4 7h8v4H4z" opacity="0.75"/><path d="M6 3h4v4H6z" opacity="0.5"/></svg>'},xe=t=>{const s=/([\d.]+)\s*GB/.exec(t);return s?parseFloat(s[1]):NaN},oe=t=>{var s;try{return(((s=localStorage.getItem(`zt-chat-${t}`))==null?void 0:s.length)??0)>2}catch{return!1}},w=new Set;async function we(t){var s;if(!(!("gpu"in navigator)||!((s=navigator.storage)!=null&&s.getDirectory)))try{const{isModelCached:o}=await ce(async()=>{const{isModelCached:u}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:u}},__vite__mapDeps([0,1,2]));for(const{spec:u}of ie)await o(u)&&w.add(u.id);t()}catch{}}function $e(){var j,W;const t=document.getElementById("model-browser");if(t===null)return;const s=t;t.innerHTML=`
|
| 3 |
+
<div class="cs-splash" aria-hidden="true">
|
| 4 |
+
<div class="cs-splash-ring" aria-hidden="true"></div>
|
| 5 |
+
<svg class="cs-emblem" viewBox="0 0 96 96" fill="none">
|
| 6 |
+
<path d="M48 6 L90 48 L48 90 L6 48 Z" stroke="currentColor" stroke-width="3"/>
|
| 7 |
+
<path d="M48 20 L76 48 L48 76 L20 48 Z" stroke="currentColor" stroke-width="1.4" opacity="0.55"/>
|
| 8 |
+
<path d="M30 48 h12 l6 -10 l6 20 l6 -10 h6" stroke="currentColor" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
|
| 9 |
+
<path d="M48 2 l4 4 l-4 4 l-4 -4 Z M48 86 l4 4 l-4 4 l-4 -4 Z M2 48 l4 -4 l4 4 l-4 4 Z M86 48 l4 -4 l4 4 l-4 4 Z" fill="currentColor"/>
|
| 10 |
+
</svg>
|
| 11 |
+
<span class="cs-logo">zero<b>-tvm</b></span>
|
| 12 |
+
<span class="cs-sub">LLM inference in the browser Β· hand-written WGSL</span>
|
| 13 |
+
</div>
|
| 14 |
+
<div class="cs-spires" aria-hidden="true"></div>
|
| 15 |
+
<div class="cs-col cs-col-l" aria-hidden="true"></div>
|
| 16 |
+
<div class="cs-col cs-col-r" aria-hidden="true"></div>
|
| 17 |
+
<div class="cs-comet" aria-hidden="true"></div>
|
| 18 |
+
<div class="cs-sigilbg" aria-hidden="true"></div>
|
| 19 |
+
<div class="cs-stream" aria-hidden="true"><i>β</i><i>β</i><i>β</i><i>β</i><i>β</i><i>β</i></div>
|
| 20 |
+
<div class="cs-fog" aria-hidden="true"><i></i><i></i></div>
|
| 21 |
+
<div class="cs-dust" aria-hidden="true"></div>
|
| 22 |
+
<div class="mb-plate">
|
| 23 |
+
<div class="cs-banner" aria-hidden="true"></div>
|
| 24 |
+
<div class="mb-name"></div>
|
| 25 |
+
<div class="mb-params"><span class="mb-sigil" aria-hidden="true"></span><span class="mb-params-text"></span></div>
|
| 26 |
+
<div class="cs-lore"></div>
|
| 27 |
+
</div>
|
| 28 |
+
<div class="mb-stage">
|
| 29 |
+
<button class="mb-arrow" data-dir="-1" aria-label="Previous model">‹</button>
|
| 30 |
+
<div class="mb-art">
|
| 31 |
+
<div class="mb-pedestal" aria-hidden="true"></div>
|
| 32 |
+
<canvas class="mb-mascot" aria-hidden="true"></canvas>
|
| 33 |
+
</div>
|
| 34 |
+
<button class="mb-arrow" data-dir="1" aria-label="Next model">›</button>
|
| 35 |
+
</div>
|
| 36 |
+
<aside class="mb-info">
|
| 37 |
+
<div class="mb-panel">
|
| 38 |
+
<div class="mb-row-label mb-variants-label">Quantisation</div>
|
| 39 |
+
<div class="mb-variants" role="tablist" aria-label="Quantisation"></div>
|
| 40 |
+
<dl class="mb-stats"></dl>
|
| 41 |
+
<ul class="mb-abilities"></ul>
|
| 42 |
+
<div class="mb-row-label mb-modes-label" hidden>Memory build</div>
|
| 43 |
+
<div class="mb-modes" role="tablist" aria-label="Memory build"></div>
|
| 44 |
+
<div class="mb-row-label mb-ctxs-label" hidden>Context</div>
|
| 45 |
+
<div class="mb-modes mb-ctxs" role="tablist" aria-label="Context"></div>
|
| 46 |
+
<p class="mb-cached" hidden>Already on this device β opens in seconds</p>
|
| 47 |
+
<p class="mb-save" hidden>β Saved conversation β it continues where you left it</p>
|
| 48 |
+
<p class="mb-ram"></p>
|
| 49 |
+
</div>
|
| 50 |
+
</aside>
|
| 51 |
+
${"gpu"in navigator?'<div class="cs-deeds" role="list" aria-label="Deeds"></div>':""}
|
| 52 |
+
<div class="cs-roster-head" aria-hidden="true">Roster</div>
|
| 53 |
+
<div class="mb-dots mb-roster" role="tablist" aria-label="Models"></div>
|
| 54 |
+
<div class="cs-enter">
|
| 55 |
+
<a class="mb-cta btn btn-primary">Enter chat βΈ</a>
|
| 56 |
+
<a class="mb-cta-room">β Enter & open a room β serve this model to other machines</a>
|
| 57 |
+
</div>
|
| 58 |
+
<div class="cs-live" aria-live="polite"></div>
|
| 59 |
+
<div class="cs-wipe" aria-hidden="true"></div>
|
| 60 |
+
<div class="cs-engage" aria-hidden="true"></div>
|
| 61 |
+
<div class="cs-borderline-t" aria-hidden="true"></div>
|
| 62 |
+
<div class="cs-borderline-b" aria-hidden="true"></div>
|
| 63 |
+
<div class="cs-corner cs-corner-l" aria-hidden="true">registry-rendered Β· measured, or marked est</div>
|
| 64 |
+
<div class="cs-corner cs-corner-r" aria-hidden="true">WebGPU Β· hand-written WGSL</div>
|
| 65 |
+
${"gpu"in navigator?"":'<p class="note cs-note">This browser has no WebGPU β the chat needs Chrome, Edge, or another WebGPU-enabled browser.</p>'}
|
| 66 |
+
`;{const e=t.querySelector(".cs-splash");if(matchMedia("(prefers-reduced-motion: reduce)").matches||sessionStorage.getItem("zt-intro"))e.remove();else{sessionStorage.setItem("zt-intro","1"),e.addEventListener("animationend",i=>{i.target===e&&e.remove()});const n=()=>e.remove();e.addEventListener("pointerdown",n),window.addEventListener("keydown",n,{once:!0})}}const o=e=>t.querySelector(e),u=o(".mb-mascot"),M=o(".mb-dots");let y=0,b=0,$=0,k=0,l=null;const _=(e,a)=>a&&e.moe?a/(e.moe.experts+(e.sharedExpertIndex>=0?1:0)):0;M.innerHTML=f.map((e,a)=>{const n=e.variants[0].spec,i=se(n);return`<button class="mb-dot" data-i="${a}" role="tab" title="${e.name}" aria-label="${e.name}">
|
| 67 |
+
<span class="mb-dot-face" aria-hidden="true"></span>
|
| 68 |
+
<span class="mb-dot-text"><b>${e.name.replace(/-Instruct.*$/,"")}</b><i>${e.params}</i></span>
|
| 69 |
+
<span class="mb-dot-sigil" style="color:${i.accent}" aria-hidden="true">${z[H(n)]??""}</span>
|
| 70 |
+
</button>`}).join("");async function D(){const e=document.createElement("canvas");e.style.cssText="position:absolute;left:-9999px;width:96px;height:96px",document.body.appendChild(e);const a=await ae(e,f[0].variants[0].spec);if(!a){e.remove();return}for(let n=0;n<f.length;n++){const i=f[n].variants[0].spec;a.setSpec(i,f[n].variants.some(h=>w.has(h.spec.id))),await new Promise(h=>setTimeout(h,40));const p=await a.snapshot(!0),m=s.querySelector(`.mb-dot[data-i="${n}"] .mb-dot-face`);m&&m.style.setProperty("--thumb",`url("${p}")`)}a.destroy(),e.remove()}D();function L(){const e=f[y],a=e.variants[b],n=I(a.spec);o(".mb-name").textContent=e.name,o(".mb-params-text").textContent=e.params,o(".mb-sigil").innerHTML=z[H(a.spec)]??"",o(".cs-lore").textContent=ge(a.spec);{const r=s.querySelector(".cs-sigilbg"),c=z[H(a.spec)];if(r&&c){const v=`url("data:image/svg+xml,${encodeURIComponent(c)}")`;r.style.webkitMaskImage=v,r.style.maskImage=v}}const i=parseFloat(n.rateLabel.replace(/[^0-9.]/g,""));s.style.setProperty("--cs-tempo",String(Number.isFinite(i)?Math.min(2.2,.7+i/140):1)),o(".mb-art").toggleAttribute("data-armed",w.has(a.spec.id));const p=s.querySelector(".cs-live");p&&(p.textContent=`${e.name}, ${e.params}`);const m=se(a.spec);s.style.setProperty("--cs-accent",m.accent),s.style.setProperty("--cs-accent-hi",m.accentHi);const h=n.poolModes??[],d=h[$]??h[0],g=G(a.spec),x=g[k]??g[0];{const r=[];a.param&&r.push(`model=${a.param}`),d&&d.slots&&r.push(`pool=${d.slots}`),x.tokens!==a.spec.maxContext&&r.push(`ctx=${x.tokens}`),o(".mb-cta").href=`zero-tvm.html${r.length?"?"+r.join("&"):""}`,o(".mb-cta-room").href=`share.html${r.length?"?"+r.join("&"):""}`}o(".mb-modes").innerHTML=h.length<2?"":h.map((r,c)=>`<button class="mb-variant mb-mode" data-m="${c}" role="tab" aria-selected="${c===$}"><span class="mb-gauge" aria-hidden="true">${"β".repeat(h.length-c)}${"β".repeat(c)}</span>${r.label}</button>`).join(""),o(".mb-modes-label").hidden=h.length<2,o(".mb-ctxs").innerHTML=g.length<2?"":g.map((r,c)=>`<button class="mb-variant mb-ctx" data-x="${c}" role="tab" aria-selected="${c===k}">${r.name} Β· ${P(r.tokens)} Β· ~${ne(a.spec,r.tokens,F)}</button>`).join(""),o(".mb-ctxs-label").hidden=g.length<2,o(".mb-variants").innerHTML=e.variants.length<2?"":e.variants.map((r,c)=>`<button class="mb-variant" data-v="${c}" role="tab" aria-selected="${c===b}"${w.has(r.spec.id)?" data-cached":""}>${r.label}</button>`).join(""),o(".mb-variants-label").hidden=e.variants.length<2;const E=[["Weights",n.sizeLabel],["Context",`${P(x.tokens)} tokens`]];n.rateLabel&&!(d&&d.slots)&&E.push(["Speed",`${n.rateLabel} <span class="mb-hw">M2 Max</span>`]);let T=NaN;{const r=xe(d&&d.slots?d.label:n.sizeLabel);if(Number.isFinite(r)){const c=a.spec,v=c.layerKinds.filter(A=>A==="gdn").length,S=v?4*v*((c.gdnConvK-1)*c.gdnQkvDim*2+c.gdnVHeads*c.gdnStatePerHead*4)/2**30:0;T=r+x.tokens*le(c,F)/2**30+S,E.push(["Footprint",`~${T.toFixed(1)} GB <span class="mb-hw">weights + KV${S?" + state":""}</span>`])}}const U=Math.min(1,x.tokens/a.spec.maxSeq),N=d&&d.slots&&_(a.spec,d.slots)||1,V=(r,c)=>`<span class="mb-bar" title="${c}"><i style="width:${Math.round(r*100)}%"></i></span>`;if(o(".mb-stats").innerHTML=E.map(([r,c])=>{const v=re[r]?`<span class="mb-stat-ico" aria-hidden="true">${re[r]}</span>`:"",S=r==="Context"?V(U,`${Math.round(U*100)}% of the checkpoint's trained ${P(a.spec.maxSeq)} window`):r==="Weights"&&N<1?V(N,`${Math.round(N*100)}% of experts resident in this build`):"";return`<div><dt>${v}${r}</dt><dd>${c}${S}</dd></div>`}).join(""),!matchMedia("(prefers-reduced-motion: reduce)").matches)for(const r of s.querySelectorAll(".mb-stats dd")){const c=r.innerHTML,v=/^([^0-9]*)([0-9]+(?:\.[0-9]+)?)([\s\S]*)$/.exec(r.textContent??"");if(!v)continue;const S=parseFloat(v[2]),A=v[2].includes(".")?1:0,me=performance.now(),te=()=>{const B=Math.min(1,(performance.now()-me)/300),ue=1-(1-B)*(1-B);r.textContent=`${v[1]}${(S*(.4+.6*ue)).toFixed(A)}${v[3]}`,B<1?requestAnimationFrame(te):r.innerHTML=c};requestAnimationFrame(te)}o(".mb-abilities").innerHTML=be(a.spec).map(r=>`<li><b>${r.name}</b><span>${r.desc}</span></li>`).join("");const Z=o(".mb-ram"),Q=Number.isFinite(T)?Math.round(T*1.15):NaN,X=d&&d.slots?d.note??"":n.ramNote??"",Y=X.replace(/needs\s*~?[\d.]+\s*GB free RAM/i,"").replace(/^[\sβ-]+/,"").trim(),J=Number.isFinite(Q)?`needs ~${Q} GB free RAM${Y?` β ${Y}`:""}`:X,ee=x.tokens>a.spec.maxContext?`long context allocates ~${ne(a.spec,x.tokens,F)} at boot, on top of the weights`:"";Z.textContent=[J,ee].filter(Boolean).join(" β "),Z.hidden=!(J||ee);for(const r of s.querySelectorAll(".mb-dot")){r.setAttribute("aria-selected",String(Number(r.dataset.i)===y));const c=f[Number(r.dataset.i)];r.toggleAttribute("data-cached",c.variants.some(v=>w.has(v.spec.id))),r.toggleAttribute("data-save",c.variants.some(v=>oe(v.spec.id)))}const de=o(".mb-cached");de.hidden=!w.has(a.spec.id),o(".mb-save").hidden=!oe(a.spec.id),l==null||l.setSpec(he(a.spec,x.tokens),w.has(a.spec.id),d?_(a.spec,d.slots):0)}function K(){const e=s.querySelector(".cs-deeds");if(!e)return;const a=pe();e.innerHTML=ve.map(n=>{const i=`${a.has(n.id)?"Earned":"Not yet earned"}: ${n.name} β ${n.desc}`;return`<span class="cs-deed" role="listitem"${a.has(n.id)?" data-on":""} title="${n.name} β ${n.desc}" aria-label="${i}">β<i>${n.name}</i></span>`}).join("")}document.addEventListener("zt-feat",K),K();function O(){const e=s.querySelector(".cs-wipe");e&&(e.classList.remove("cs-go"),e.offsetWidth,e.classList.add("cs-go"));for(const a of[".mb-art",".mb-plate",".mb-panel"]){const n=s.querySelector(a);n&&(n.classList.remove("cs-in"),n.offsetWidth,n.classList.add("cs-in"))}}function q(e){y=(e+f.length)%f.length,b=0,$=0,k=0,L(),O()}t.addEventListener("click",e=>{const a=e.target,n=a.closest(".mb-arrow");if(n){q(y+Number(n.dataset.dir));return}const i=a.closest(".mb-dot");if(i){q(Number(i.dataset.i));return}const p=g=>{var x;(x=s.querySelector(g))==null||x.focus()},m=a.closest(".mb-mode");if(m){$=Number(m.dataset.m),L(),p(`.mb-mode[data-m="${$}"]`);return}const h=a.closest(".mb-ctx");if(h){k=Number(h.dataset.x),L(),p(`.mb-ctx[data-x="${k}"]`);return}const d=a.closest(".mb-variant");d&&(b=Number(d.dataset.v),$=0,k=0,L(),p(`.mb-variant[data-v="${b}"]`))}),t.tabIndex=0,t.setAttribute("role","application"),t.setAttribute("aria-label","Character select β Up and Down arrows change model, Enter opens the chat"),t.addEventListener("keydown",e=>{var a;s.classList.contains("cs-chatting")||(e.key==="ArrowRight"||e.key==="ArrowDown"?(e.preventDefault(),q(y+1)):e.key==="ArrowLeft"||e.key==="ArrowUp"?(e.preventDefault(),q(y-1)):e.key==="Enter"&&e.target.tagName!=="BUTTON"&&e.target.tagName!=="A"&&((a=s.querySelector(".mb-cta"))==null||a.click()))});const C=o(".mb-art");if(C.addEventListener("mouseenter",()=>l==null?void 0:l.setHover(!0)),C.addEventListener("mouseleave",()=>l==null?void 0:l.setHover(!1)),M.addEventListener("mouseenter",()=>{C.style.rotate="y -6deg",l==null||l.setHover(!0)},!0),M.addEventListener("mouseleave",()=>{C.style.rotate="none",l==null||l.setHover(!1)},!0),!matchMedia("(prefers-reduced-motion: reduce)").matches){const e=[[s.querySelector(".cs-spires"),6,2],[s.querySelector(".cs-col-l"),34,8],[s.querySelector(".cs-col-r"),34,8],[s.querySelector(".cs-fog"),26,10],[s.querySelector(".cs-dust"),12,5],[s.querySelector(".cs-sigilbg"),9,4],[s.querySelector(".mb-art"),-7,-3]];s.addEventListener("pointermove",a=>{const n=s.getBoundingClientRect(),i=(a.clientX-n.left)/n.width-.5,p=(a.clientY-n.top)/n.height-.5;for(const[m,h,d]of e)m&&(m.style.transform=`translate(${i*h}px, ${p*d}px)`);l==null||l.setGaze(i*2,p*2)})}(async()=>{var e;try{const a=await((e=navigator.gpu)==null?void 0:e.requestAdapter()),n=a==null?void 0:a.info,i=s.querySelector(".cs-corner-r");i&&n&&(n.vendor||n.architecture)&&(i.textContent=`realm: ${[n.vendor,n.architecture].filter(Boolean).join(" Β· ")} β online`)}catch{}})();const R=(e,a)=>{var d;if(e.metaKey||e.ctrlKey||e.shiftKey||e.button!==0)return;if(s.classList.contains("cs-chatting")){e.preventDefault();return}if(!("gpu"in navigator))return;const n=e.currentTarget.href;e.preventDefault(),matchMedia("(prefers-reduced-motion: reduce)").matches||(d=s.querySelector(".cs-engage"))==null||d.classList.add("cs-go");const i=f[y].variants[b],p=I(i.spec).poolModes??[],m=p[$]??p[0],h=G(i.spec)[k]??G(i.spec)[0];ce(async()=>{const{enterChat:g}=await import("./landing-chat-Bo__3GxplQ.js");return{enterChat:g}},__vite__mapDeps([3,4,5,2,6,7,1,8,9,10,11,12])).then(({enterChat:g})=>g({root:s,spec:i.spec,param:i.param,poolSlots:(m==null?void 0:m.slots)??0,poolLabel:m&&m.slots?m.label:"",ctxTokens:h.tokens!==i.spec.maxContext?h.tokens:0,openRoom:a,mascot:l})).catch(g=>{console.error("[landing] in-place chat failed, navigating:",g),location.href=n})};if((j=s.querySelector(".mb-cta"))==null||j.addEventListener("click",e=>R(e,!1)),(W=s.querySelector(".mb-cta-room"))==null||W.addEventListener("click",e=>R(e,!0)),!matchMedia("(prefers-reduced-motion: reduce)").matches){let e=0,a=0;const n=()=>["cs-summoning","cs-thinking","cs-generating"].some(m=>s.classList.contains(m)),i=()=>{if(n()){a=window.setTimeout(i,5e4);return}s.classList.add("cs-asleep"),l==null||l.setMood("sleepy")},p=()=>{s.classList.contains("cs-asleep")&&(s.classList.remove("cs-asleep"),n()||l==null||l.setMood("idle")),s.classList.remove("cs-idle"),clearTimeout(e),clearTimeout(a),e=window.setTimeout(()=>s.classList.add("cs-idle"),18e3),a=window.setTimeout(i,5e4)};for(const m of["pointermove","pointerdown","keydown"])s.addEventListener(m,p);p()}L(),O(),we(()=>{L(),D()}),ae(u,f[y].variants[b].spec).then(e=>{if(!e){C.style.display="none";return}l=e,e.setSpec(f[y].variants[b].spec,w.has(f[y].variants[b].spec.id))})}$e();export{z as L,ge as a,H as l};
|
assets/landing-chat-Bo__3GxplQ.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/landing-room-BSBnFYP4bg.js","assets/mascot-BCKWddkWrV.js","assets/model-registry-BoK-ObgACo.js","assets/model-select-DhhV9mCi0T.js","assets/weight-loader-ByOLMd9f5C.js","assets/room-host-C6R3zseuU5.js","assets/chat-ui-CQAxV1po6S.js","assets/preload-helper-ckwbz45pO3.js","assets/feats-dlBfBxqpI8.js"])))=>i.map(i=>d[i]);
|
| 2 |
+
import{_ as x}from"./preload-helper-ckwbz45pO3.js";import{m as y}from"./mascot-BCKWddkWrV.js";import{s as S,m as $}from"./model-registry-BoK-ObgACo.js";import{b as C,s as w,w as P}from"./chat-flow-eTAzKAlIYe.js";import{l as L,L as T,a as E}from"./index-BqNbylMY24.js";import{recordFeat as d}from"./feats-dlBfBxqpI8.js";import"./model-select-DhhV9mCi0T.js";import"./weight-loader-ByOLMd9f5C.js";import"./engine-core-CLR3cxsqf4.js";import"./variants-De-VVc9BRz.js";import"./loading-ui-CjCwSRuXvh.js";import"./chat-ui-CQAxV1po6S.js";function z(){let o=!1;const s=[],t=[],a=()=>{for(const i of t)i(o)};return{acquire(){return o?new Promise(i=>s.push(()=>{i()})):(o=!0,a(),Promise.resolve())},release(){const i=s.shift();if(i){i();return}o=!1,a()},held:()=>o,onChange(i){t.push(i)}}}const M='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l14-7-7 14-2-5-5-2z"/></svg>',_='<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="7" y="7" width="10" height="10" rx="2"/></svg>',W='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>';function B(o,s,t){const a=T[L(o)]??"",i=s.params.split(/[\sΒ·]/)[0],m=t!==s.params?`Build: ${t}`:s.ramNote??"";return`
|
| 3 |
+
<div class="cs-chat-head">
|
| 4 |
+
<span class="cs-chat-sigil" aria-hidden="true">${a}</span>
|
| 5 |
+
<div class="cs-chat-id">
|
| 6 |
+
<b>${s.name}</b>
|
| 7 |
+
<i>${t}</i>
|
| 8 |
+
</div>
|
| 9 |
+
<span class="badge" id="badge"><span class="dot"></span><span id="badge-text">Summoning</span></span>
|
| 10 |
+
<button class="cs-chat-tool" id="new-chat-btn" type="button" title="New chat" disabled>New chat</button>
|
| 11 |
+
<a class="cs-chat-tool" id="cs-roster-link" href="/" title="Back to the character select">β¨ Roster</a>
|
| 12 |
+
</div>
|
| 13 |
+
<div class="cs-boot" id="progress-wrap">
|
| 14 |
+
<div class="cs-boot-title" id="loading-title">Summoning ${s.name}</div>
|
| 15 |
+
<div class="cs-boot-status" id="progress-status" aria-live="polite">Preparingβ¦</div>
|
| 16 |
+
<div class="cs-boot-track"><i id="progress-bar"></i></div>
|
| 17 |
+
<div class="cs-boot-detail" id="progress-detail">${s.sizeLabel} Β· cached after first load β next visit starts in seconds</div>
|
| 18 |
+
${m?`<div class="cs-boot-note">${m}</div>`:""}
|
| 19 |
+
<details class="cs-boot-log"><summary>Rite log</summary><pre id="progress-log"></pre></details>
|
| 20 |
+
<div class="cs-boot-error" id="loading-error"></div>
|
| 21 |
+
</div>
|
| 22 |
+
<main class="chat-main" id="chat-main">
|
| 23 |
+
<div class="chat-inner">
|
| 24 |
+
<div class="welcome cs-welcome cs-wait" id="welcome">
|
| 25 |
+
<div class="cs-welcome-title">Speak with ${s.name}</div>
|
| 26 |
+
<div class="cs-welcome-lore">${E(o)} Prompts, tokens and the KV cache stay in this tab.</div>
|
| 27 |
+
<div class="cs-sug-grid" id="suggest-grid">
|
| 28 |
+
<button class="suggest" data-prompt="What is zero-tvm, and what makes it different from other ways of running an LLM in a browser?">What is zero-tvm?</button>
|
| 29 |
+
<button class="suggest" data-prompt="Explain WebGPU compute shaders in three sentences.">Explain WebGPU compute shaders</button>
|
| 30 |
+
<button class="suggest" data-prompt="Write a TypeScript memoized fibonacci with a short test block that prints fib(10), fib(20), and fib(30).">Write a memoized fibonacci in TypeScript</button>
|
| 31 |
+
<button class="suggest" data-prompt="Summarize how a transformer decode step works, focusing on what the KV cache actually stores.">What does the KV cache actually store?</button>
|
| 32 |
+
<button class="suggest" data-prompt="List four kinds of questions where a ${i} model like ${s.name} is likely to fall short compared to a frontier LLM, with one-sentence reasons.">Where does a ${i} model fall short?</button>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
<div id="messages"></div>
|
| 36 |
+
</div>
|
| 37 |
+
<button class="scroll-fab" id="scroll-fab" title="Scroll to bottom" aria-label="Scroll to bottom">${W}</button>
|
| 38 |
+
</main>
|
| 39 |
+
<div class="composer-wrap">
|
| 40 |
+
<form class="composer" id="composer">
|
| 41 |
+
<textarea id="inp" rows="1" placeholder="Summoningβ¦" aria-label="Message" disabled></textarea>
|
| 42 |
+
<button class="composer-btn" data-variant="send" id="btn" type="submit" disabled aria-label="Send">${M}</button>
|
| 43 |
+
<button class="composer-btn" data-variant="stop" id="stop-btn" type="button" hidden aria-label="Stop">${_}</button>
|
| 44 |
+
</form>
|
| 45 |
+
<div class="composer-hint">
|
| 46 |
+
<span id="ctx-hint">Zero TVM Β· 10 WGSL kernel roles</span>
|
| 47 |
+
<span class="cs-chat-stats" id="stats"></span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>`}async function N(o){var v,b,f;const{root:s,mascot:t}=o,a=o.ctxTokens?S(o.spec,o.ctxTokens):o.spec,i=$(a),m=[o.poolLabel||i.params,a.maxContext!==o.spec.maxContext?`${Math.round(a.maxContext/1024)}k ctx`:""].filter(Boolean).join(" Β· ");{const{accent:e,accentHi:c}=y(a),r=document.documentElement.style;r.setProperty("--accent",e),r.setProperty("--accent-hi",c),r.setProperty("--accent-2",c),r.setProperty("--accent-dim",`${e}22`),r.setProperty("--accent-tint",`${e}1f`)}document.title=`${i.name} Β· zero-tvm`;const n=document.createElement("section");n.className="cs-chat",n.setAttribute("role","region"),n.setAttribute("aria-label",`Chat with ${i.name}`),n.innerHTML=B(a,i,m),s.appendChild(n),s.classList.add("cs-chatting"),n.tabIndex=-1,n.focus(),(v=n.querySelector("#cs-roster-link"))==null||v.addEventListener("click",e=>{e.preventDefault(),location.reload()});let g=0;const u=e=>{clearInterval(g),s.classList.toggle("cs-summoning",e==="summon"),s.classList.toggle("cs-thinking",e==="thinking"),s.classList.toggle("cs-generating",e==="generating"),t==null||t.setHover(e==="thinking"),t==null||t.setMood(e==="generating"?"talking":e==="thinking"?"thinking":"idle"),e==="summon"&&(g=window.setInterval(()=>t==null?void 0:t.pulse(),640))};u("summon");const l=await C({spec:a,poolSlots:o.poolSlots,search:location.search,onDeviceLost:e=>{w(`GPU device lost: ${e.message||e.reason}. Reload the page to recover.`)}});if(!l.ok){u("idle"),s.classList.add("cs-boot-failed");const e=n.querySelector("#loading-title");e&&(e.textContent="The summoning failed"),w(l.reason,()=>{n.remove(),s.classList.remove("cs-chatting","cs-boot-failed"),N(o)});return}u("idle"),s.classList.add("cs-ready"),(b=n.querySelector(".cs-boot"))==null||b.classList.add("cs-done"),(f=n.querySelector("#welcome"))==null||f.classList.remove("cs-wait"),t==null||t.setMood("greet"),t==null||t.pulse(),setTimeout(()=>{!s.classList.contains("cs-thinking")&&!s.classList.contains("cs-generating")&&(t==null||t.setMood("idle"))},1600),d("first-summon");const p=L(a);(p==="dense"||p==="hybrid"||p==="moe")&&d(`lane-${p}`),o.poolSlots&&d("pooled"),o.ctxTokens&&d("long-ctx");{const e=/([\d.]+)\s*GB/.exec(o.poolLabel||i.sizeLabel);(e?parseFloat(e[1]):0)+a.maxContext*a.kvBytesPerToken/2**30>=10&&d("heavy")}const h=z();P({spec:a,tokenizer:l.tokenizer,engine:l.engine,lock:h,onToken:()=>t==null?void 0:t.pulse(),onPhase:e=>u(e),commands:{roster:()=>location.reload()}}),x(async()=>{const{mountRoomTool:e}=await import("./landing-room-BSBnFYP4bg.js");return{mountRoomTool:e}},__vite__mapDeps([0,1,2,3,4,5,6,7,8])).then(({mountRoomTool:e})=>e({root:s,panel:n,spec:a,param:o.param,engine:l.engine,tokenizer:l.tokenizer,mascot:t,lock:h,poolSlots:o.poolSlots,openStrip:o.openRoom===!0})).catch(e=>console.warn("[landing] room tool failed to mount:",e));{const e=n.querySelector("#inp");let c=0;const r=()=>{s.classList.remove("cs-listening"),s.classList.contains("cs-thinking")||t==null||t.setHover(!1)},k=()=>{s.classList.add("cs-listening"),t==null||t.setHover(!0),clearTimeout(c),c=window.setTimeout(r,1600)};e==null||e.addEventListener("input",k),e==null||e.addEventListener("focus",k),e==null||e.addEventListener("blur",()=>{clearTimeout(c),r()})}}export{N as enterChat};
|
assets/landing-room-BSBnFYP4bg.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{a as y}from"./mascot-BCKWddkWrV.js";import{m as f}from"./model-registry-BoK-ObgACo.js";import{buildChatPromptFor as k}from"./model-select-DhhV9mCi0T.js";import{h as C}from"./room-host-C6R3zseuU5.js";import{recordFeat as w}from"./feats-dlBfBxqpI8.js";import"./weight-loader-ByOLMd9f5C.js";import"./chat-ui-CQAxV1po6S.js";import"./preload-helper-ckwbz45pO3.js";function A(t){const h=t.panel.querySelector(".cs-chat-head");if(!h)return;const v=f(t.spec),s=document.createElement("button");s.type="button",s.className="cs-chat-tool",s.id="room-btn",s.textContent="β Room",s.title="Serve this model to other machines",h.insertBefore(s,h.querySelector("#new-chat-btn"));const c=document.createElement("div");c.className="cs-room",c.hidden=!t.openStrip,c.innerHTML=`
|
| 2 |
+
<div class="cs-room-consent">
|
| 3 |
+
<p>Open a room and whoever has the link chats with <b>${v.name}</b> running on
|
| 4 |
+
THIS machine. Their prompts run on your GPU; every request is listed here as it
|
| 5 |
+
arrives. Guests can also copy the model's cached weights from this machine to
|
| 6 |
+
run it locally. Keep this tab in the foreground while serving.</p>
|
| 7 |
+
<button type="button" class="cs-chat-tool" id="room-open">Open room β</button>
|
| 8 |
+
</div>
|
| 9 |
+
<div class="cs-room-live" hidden>
|
| 10 |
+
<div class="cs-room-linkrow">
|
| 11 |
+
<input id="room-link" readonly aria-label="Room link">
|
| 12 |
+
<button type="button" class="cs-chat-tool" id="room-copy">Copy</button>
|
| 13 |
+
<button type="button" class="cs-chat-tool" id="room-close">Close room</button>
|
| 14 |
+
</div>
|
| 15 |
+
<div class="cs-room-members" id="room-members" aria-live="polite">waiting for guestsβ¦</div>
|
| 16 |
+
<ul class="cs-room-log" id="room-log" role="log" aria-live="polite" aria-label="Guest requests"></ul>
|
| 17 |
+
</div>`,h.insertAdjacentElement("afterend",c);const n=l=>c.querySelector(l);let a=null,d=null;const u=[];function b(l){var i;const e=t.root.querySelector(".mb-art");if(e){for(;u.length<Math.min(l,6);){const o=u.length,r=document.createElement("canvas");r.className=`cs-guest-mascot cs-guest-${o}`,r.setAttribute("aria-hidden","true"),e.appendChild(r);const p={canvas:r,handle:null};u.push(p),y(r,t.spec).then(m=>{if(!m){r.remove();return}if(!r.isConnected){m.destroy();return}p.handle=m})}for(;u.length>l;){const o=u.pop();(i=o==null?void 0:o.handle)==null||i.destroy(),o==null||o.canvas.remove()}}}s.setAttribute("aria-expanded",String(!c.hidden)),s.addEventListener("click",()=>{c.hidden=!c.hidden,s.setAttribute("aria-expanded",String(!c.hidden))}),n("#room-open").addEventListener("click",()=>{var l;a||(a=C({spec:t.spec,brand:t.poolSlots?{...v,rateLabel:""}:v,param:t.param,engine:t.engine,tokenizer:t.tokenizer,lock:t.lock,encode:e=>k(t.spec,e,t.tokenizer),ui:{row:(e,i)=>{var m;const o=n("#room-log"),r=document.createElement("li");for(r.innerHTML="<b></b><span></span><i></i>",r.querySelector("b").textContent=e.slice(0,8),r.querySelector("span").textContent=i,o.prepend(r);o.children.length>6;)(m=o.lastChild)==null||m.remove();const p=r.querySelector("i");return g=>{p.textContent=g}},onMembers:({hosts:e,guests:i,connected:o})=>{n("#room-members").textContent=o===0?"waiting for guestsβ¦":`${e} ${e===1?"machine":"machines"} serving Β· ${i} ${i===1?"guest":"guests"} in the room`,b(o)},onToken:()=>{var e;return(e=t.mascot)==null?void 0:e.pulse()},onBusy:e=>{var i;(i=t.mascot)==null||i.setMood(e?"talking":"idle"),t.root.classList.toggle("cs-generating",e)}}}),n("#room-link").value=a.link,n(".cs-room-consent").hidden=!0,n(".cs-room-live").hidden=!1,s.classList.add("cs-tool-live"),w("room"),(l=navigator.wakeLock)==null||l.request("screen").then(e=>{d=e}).catch(()=>{}))}),n("#room-copy").addEventListener("click",()=>{a&&(navigator.clipboard.writeText(a.link),n("#room-copy").textContent="Copied",setTimeout(()=>{n("#room-copy").textContent="Copy"},1200))}),n("#room-close").addEventListener("click",()=>{a==null||a.close(),a=null,d==null||d.release().catch(()=>{}),d=null,b(0),n(".cs-room-live").hidden=!0,n(".cs-room-consent").hidden=!1,s.classList.remove("cs-tool-live"),n("#room-members").textContent="waiting for guestsβ¦"})}export{A as mountRoomTool};
|
assets/loading-ui-CjCwSRuXvh.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{l as P,f as y}from"./weight-loader-ByOLMd9f5C.js";import{loadTokenizerFor as B,buildChatPromptFor as w}from"./model-select-DhhV9mCi0T.js";import{allocKVPages as G,buildDecodeEngine as U}from"./engine-core-CLR3cxsqf4.js";import{SCALAR_VARIANTS as E}from"./variants-De-VVc9BRz.js";import{P as $}from"./model-registry-BoK-ObgACo.js";function n(e){const o=document.getElementById("progress-log");o&&(o.textContent+=e+`
|
| 2 |
+
`,o.scrollTop=o.scrollHeight)}function i(e,o="idle"){const t=document.getElementById("badge");if(!t)return;const u=document.getElementById("badge-text");u?u.textContent=e:t.textContent=e,t.className=`badge ${o}`}function a(e,o,t){const u=document.getElementById("progress-bar"),d=document.getElementById("progress-status"),s=document.getElementById("progress-detail");u&&(u.style.width=`${Math.min(100,e)}%`),o&&d&&(d.textContent=o),t!==void 0&&s&&(s.textContent=t)}function k(){var e;(e=document.getElementById("progress-wrap"))==null||e.classList.add("active")}function M(){var e;(e=document.getElementById("progress-wrap"))==null||e.classList.remove("active")}function m(e){return e>=1e9?(e/1e9).toFixed(1)+" GB":e>=1e6?(e/1e6).toFixed(0)+" MB":e>=1e3?(e/1e3).toFixed(0)+" KB":e+" B"}async function S(e){try{const o=e.createBuffer({size:16,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC}),t=e.createBuffer({size:16,usage:GPUBufferUsage.MAP_READ|GPUBufferUsage.COPY_DST}),u=e.createShaderModule({code:`enable subgroups;
|
| 3 |
+
@group(0) @binding(0) var<storage, read_write> out : array<u32>;
|
| 4 |
+
@compute @workgroup_size(32, 1, 1)
|
| 5 |
+
fn probe(@builtin(local_invocation_id) tid : vec3<u32>, @builtin(subgroup_size) s : u32) {
|
| 6 |
+
if (tid.x == 0u) { out[0] = s; }
|
| 7 |
+
}`}),d=e.createComputePipeline({layout:"auto",compute:{module:u,entryPoint:"probe"}}),s=e.createBindGroup({layout:d.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:o}}]}),g=e.createCommandEncoder(),l=g.beginComputePass();l.setPipeline(d),l.setBindGroup(0,s),l.dispatchWorkgroups(1),l.end(),g.copyBufferToBuffer(o,0,t,0,16),e.queue.submit([g.finish()]),await t.mapAsync(GPUMapMode.READ);const f=new Uint32Array(t.getMappedRange().slice(0))[0];t.unmap();const c=f===32;return n(`subgroups feature: yes Β· probed size: ${f} Β· path: ${c?"_sg":"scalar"}`),o.destroy(),t.destroy(),c}catch(o){return n(`subgroups probe failed: ${o} Β· falling back to scalar`),!1}}async function L(e={}){const o=e.spec??$;if(!navigator.gpu)return i("No WebGPU","error"),a(0,"No WebGPU available"),{ok:!1,reason:"WebGPU is not available in this browser"};k(),i("Initializing...","loading"),a(0,"Requesting GPU access...");const t=await navigator.gpu.requestAdapter({powerPreference:"high-performance"});if(!t)return i("No GPU","error"),a(0,"No GPU adapter found"),{ok:!1,reason:"No GPU adapter found"};const u=["shader-f16"];for(const r of e.optionalFeatures??[])t.features.has(r)&&u.push(r);const d={};t.limits&&(d.maxStorageBufferBindingSize=t.limits.maxStorageBufferBindingSize,d.maxBufferSize=t.limits.maxBufferSize);let s;try{s=await t.requestDevice({requiredFeatures:u,requiredLimits:d})}catch{return i("shader-f16 missing","error"),a(0,"GPU does not support shader-f16 β Chrome or Edge required"),{ok:!1,reason:"shader-f16 unsupported β Chrome/Edge required"}}n(`GPU ready β features: ${u.join(", ")}`),s.lost.then(r=>{var p;r.reason!=="destroyed"&&(i("GPU lost","error"),a(0,`GPU device lost: ${r.message||r.reason} β reload the page to recover`),n(`ERROR: GPU device lost (${r.reason}): ${r.message}`),(p=e.onDeviceLost)==null||p.call(e,r))});let g=!1;if((e.probeSubgroups||o.moe)&&(s.features.has("subgroups")?(a(3,"Probing GPU subgroups..."),g=await S(s)):n("subgroups feature: no Β· path: scalar"),o.moe&&!g))return i("GPU unsupported","error"),a(0,`${o.id} is a sparse MoE and needs GPU subgroups (a Chromium WebGPU feature) β this GPU/browser does not expose them. The dense models on the landing page still run.`),{ok:!1,reason:"MoE spec requires the subgroups feature"};a(5,"Loading tokenizer..."),i("Tokenizer...","loading");let l;try{l=await B(o,r=>n(r))}catch(r){return i("Tokenizer failed","error"),a(5,`Tokenizer error: ${r}`),{ok:!1,reason:`Tokenizer load failed: ${r}`}}n("Tokenizer loaded"),a(10,"Loading model weights..."),i("Downloading...","loading");let f;try{f=await P(s,r=>n(r),r=>{const p=r.totalBytes>0?Math.min(1,r.bytesLoaded/r.totalBytes):0,b=Math.round(p*100);a(10+p*80,`Downloading weights β shard ${r.shardsLoaded} / ${r.totalShards}`,`${m(r.bytesLoaded)} / ${m(r.totalBytes)} Β· ${r.mbPerSec.toFixed(0)} MB/s`+(r.etaSec>0?` Β· ~${y(r.etaSec)} left`:"")),i(`${b}%`,"loading")},o,e.layerRange,e.expertPool)}catch(r){return i("Download failed","error"),a(10,`Weight load error: ${r}`),n(`ERROR: ${r}`),{ok:!1,reason:`Weight load failed: ${r}`}}a(92,"Allocating KV cache...");let c;if(e.buildEngine)a(96,"Compiling shaders..."),c=e.buildEngine({device:s,weights:f,sgSizeOk:g,spec:o});else{n(`Allocating KV cache (${o.layers} layers Γ ${o.maxPages} pages)`);const r=G(s,o);a(96,"Compiling shaders..."),n("Compiling WGSL kernels (10 roles)");const p=o.moe&&g?{...E,subgroups:!0,matmul:"tiled"}:void 0;c=U(s,f,r,{spec:o,variants:p})}a(98,"Warming up GPU pipelines...");const h=performance.now();if(e.warmup)await e.warmup(c,l,n);else{const r=w(o,[{role:"user",content:"Hi."}],l);await c.forwardLogits(r)}return n(`Warmup forward: ${Math.round(performance.now()-h)} ms`),a(100,"Ready"),n("Ready. Zero TVM. 10 hand-written WGSL kernel roles."),i("Ready","ready"),{ok:!0,device:s,tokenizer:l,weights:f,engine:c}}export{L as bootEngine,m as formatBytes,M as hideProgress,n as log,i as setBadge,a as setProgress,k as showProgress};
|
assets/mascot-BCKWddkWrV.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{m as T}from"./model-registry-BoK-ObgACo.js";const C=`
|
| 2 |
+
struct U {
|
| 3 |
+
res: vec2<f32>, time: f32, hover: f32,
|
| 4 |
+
headR: f32, spikes: f32, coil: f32, kvh: f32,
|
| 5 |
+
experts: f32, topk: f32, sharedFused: f32, bands: f32,
|
| 6 |
+
// mood: what the model is DOING right now β 0 idle, 1 thinking (prefill),
|
| 7 |
+
// 2 talking (streaming), 3 sleepy (long idle), 4 greet (summon complete).
|
| 8 |
+
// Thinking is a face (irises up, mouth pursed, ears perked); talking routes
|
| 9 |
+
// the per-token beat into the MOUTH instead of a body bob, so the flap rate
|
| 10 |
+
// is the real token cadence; sleepy is heavy lids and slow breathing; greet
|
| 11 |
+
// is a wide smile and a happy squint, held for a beat by the page.
|
| 12 |
+
ink: f32, eyesOpen: f32, bandShift: f32, mood: f32,
|
| 13 |
+
hair: vec3<f32>, _p1: f32,
|
| 14 |
+
iris: vec3<f32>, cached: f32,
|
| 15 |
+
// tempo: idle-clock multiplier from the model's MEASURED rate β a 256 tok/s
|
| 16 |
+
// model fidgets, a 35B breathes. beat: decaying pulse bumped once per real
|
| 17 |
+
// generated token. earCtx: ear length from log2(maxContext) β the 6kβ32k
|
| 18 |
+
// raise is visible. poolFrac: experts resident / experts β the memory
|
| 19 |
+
// picker's fraction; sprites split into a close resident orbit and a far
|
| 20 |
+
// ghosted drift, and the body leans out. All four are read from the spec,
|
| 21 |
+
// the registry's measured labels, or live engine events β same rule as
|
| 22 |
+
// every other trait: the picture cannot disagree with the table.
|
| 23 |
+
tempo: f32, beat: f32, earCtx: f32, poolFrac: f32,
|
| 24 |
+
// gaze: pointer position in [-1,1], written per frame β the irises follow
|
| 25 |
+
// the hand. Suppressed while thinking (the upward drift owns the eyes).
|
| 26 |
+
// pose.x: portrait flag β one frame with no sway, no bob, no blink, so
|
| 27 |
+
// every roster snapshot is the same front-facing framing.
|
| 28 |
+
gaze: vec2<f32>, pose: vec2<f32>,
|
| 29 |
+
};
|
| 30 |
+
@group(0) @binding(0) var<uniform> u: U;
|
| 31 |
+
|
| 32 |
+
fn rot(a: f32) -> mat2x2<f32> { let c = cos(a); let s = sin(a); return mat2x2<f32>(c, -s, s, c); }
|
| 33 |
+
fn smin(a: f32, b: f32, k: f32) -> f32 {
|
| 34 |
+
let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
| 35 |
+
return mix(b, a, h) - k * h * (1.0 - h);
|
| 36 |
+
}
|
| 37 |
+
fn sdEllip(p: vec3<f32>, r: vec3<f32>) -> f32 {
|
| 38 |
+
let k0 = length(p / r);
|
| 39 |
+
let k1 = length(p / (r * r));
|
| 40 |
+
return k0 * (k0 - 1.0) / k1;
|
| 41 |
+
}
|
| 42 |
+
fn sdCap(p: vec3<f32>, h: f32, r: f32) -> f32 {
|
| 43 |
+
return length(vec3<f32>(p.x, p.y - clamp(p.y, -h, h), p.z)) - r;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// x = distance, y = material
|
| 47 |
+
// 0 skin 1 hair 2 eye white 3 iris 4 sprite
|
| 48 |
+
fn map2(pIn: vec3<f32>) -> vec2<f32> {
|
| 49 |
+
var p = pIn;
|
| 50 |
+
let T = u.time * u.tempo;
|
| 51 |
+
let talking = step(1.5, u.mood) * (1.0 - step(2.5, u.mood));
|
| 52 |
+
let thinking = step(0.5, u.mood) * (1.0 - step(1.5, u.mood));
|
| 53 |
+
let sleepy = step(2.5, u.mood) * (1.0 - step(3.5, u.mood));
|
| 54 |
+
let greet = step(3.5, u.mood);
|
| 55 |
+
// Idle bob always β slower and deeper while asleep (breathing); the token
|
| 56 |
+
// pulse bobs the BODY only when not talking β while talking the same beat
|
| 57 |
+
// drives the mouth instead (below).
|
| 58 |
+
let live = 1.0 - u.pose.x; // 0 while posing a portrait
|
| 59 |
+
p.y = p.y - (sin(T * mix(1.2, 0.55, sleepy)) * mix(0.018, 0.028, sleepy)
|
| 60 |
+
+ u.beat * 0.020 * (1.0 - talking)) * live;
|
| 61 |
+
let m = rot(sin(T * 0.4) * 0.42 * live); // sway, never a full spin
|
| 62 |
+
let xz = m * vec2<f32>(p.x, p.z);
|
| 63 |
+
p = vec3<f32>(xz.x, p.y, xz.y);
|
| 64 |
+
|
| 65 |
+
let R = u.headR;
|
| 66 |
+
let hy = R * 0.62; // head centre
|
| 67 |
+
var q = p; q.x = abs(q.x); // opSymX β free symmetry
|
| 68 |
+
|
| 69 |
+
// CHIBI PROPORTION. The head is the character; the body is a hint under it.
|
| 70 |
+
var d = sdEllip(p - vec3<f32>(0.0, hy, 0.0), vec3<f32>(R, R * 1.02, R * 0.96));
|
| 71 |
+
var mat = 0.0;
|
| 72 |
+
|
| 73 |
+
// Body + arms, deliberately small.
|
| 74 |
+
// Memory mode: fewer experts resident, leaner figure. 12% at the quarter
|
| 75 |
+
// pool β visible beside the full-mode sibling, not a different character.
|
| 76 |
+
let lean = select(0.0, (1.0 - u.poolFrac) * 0.12, u.poolFrac > 0.0);
|
| 77 |
+
let body = sdCap(p - vec3<f32>(0.0, -R * 0.72, 0.0), R * 0.30 * (1.0 - lean), R * 0.40);
|
| 78 |
+
d = smin(d, body, R * 0.18);
|
| 79 |
+
let arm = sdCap(q - vec3<f32>(R * 0.46, -R * 0.72, 0.0), R * 0.16, R * 0.14);
|
| 80 |
+
d = smin(d, arm, R * 0.06);
|
| 81 |
+
|
| 82 |
+
// HAIR: a cap over the skull, plus spikes by polar repetition so the count
|
| 83 |
+
// follows the layer count instead of being a fixed loop.
|
| 84 |
+
var hair = sdEllip(p - vec3<f32>(0.0, hy + R * 0.10, -R * 0.02), vec3<f32>(R * 1.06, R * 1.05, R * 1.04));
|
| 85 |
+
// FRINGE. Cut hair only where it would cover the face: below the fringe line
|
| 86 |
+
// AND on the front side. A flat horizontal cut put the fringe at eye level
|
| 87 |
+
// and the eyes ended up inside the hair.
|
| 88 |
+
// The fringe is POINTED, not a flat line β a horizontal cut reads as a
|
| 89 |
+
// swim cap. Modulating the cut height by angle gives it locks.
|
| 90 |
+
let fringeY = hy + R * 0.10 - abs(sin(atan2(p.x, p.z) * 3.0)) * R * 0.20;
|
| 91 |
+
hair = max(hair, -max(p.y - fringeY, -p.z));
|
| 92 |
+
{
|
| 93 |
+
let n = max(u.spikes, 3.0);
|
| 94 |
+
let sect = 6.28318 / n;
|
| 95 |
+
let a = atan2(p.z, p.x);
|
| 96 |
+
let i = round(a / sect);
|
| 97 |
+
let ang = i * sect;
|
| 98 |
+
let dir = vec3<f32>(cos(ang), 0.0, sin(ang));
|
| 99 |
+
let tip = vec3<f32>(0.0, hy + R * 0.42, 0.0) + dir * R * 0.92;
|
| 100 |
+
let spike = sdCap((p - tip) * vec3<f32>(1.0, 0.70, 1.0), R * 0.26, R * 0.115);
|
| 101 |
+
hair = smin(hair, spike, R * 0.16);
|
| 102 |
+
}
|
| 103 |
+
// Ear tufts, from the KV head count. They sit ON TOP of the head β the old
|
| 104 |
+
// placement (R*0.88 sideways) was INSIDE the R*1.06 hair shell, so the ears
|
| 105 |
+
// existed in the field and never in the picture. Length still follows
|
| 106 |
+
// kvHeads and the context ceiling; attention (hover) and thinking perk them.
|
| 107 |
+
{
|
| 108 |
+
let perk = 1.0 + 0.35 * max(u.hover, thinking);
|
| 109 |
+
let eh = R * 0.42 * clamp(u.kvh / 16.0, 0.65, 1.2) * (0.8 + 0.5 * u.earCtx) * perk;
|
| 110 |
+
let t = sdEllip(q - vec3<f32>(R * 0.58, hy + R * 0.92 + eh * 0.35, 0.0),
|
| 111 |
+
vec3<f32>(R * 0.19, eh, R * 0.16));
|
| 112 |
+
hair = smin(hair, t, R * 0.10);
|
| 113 |
+
}
|
| 114 |
+
if (hair < d) { d = hair; mat = 1.0; }
|
| 115 |
+
|
| 116 |
+
// Recurrent tail.
|
| 117 |
+
if (u.coil > 0.01) {
|
| 118 |
+
for (var i = 0.0; i < 5.0; i = i + 1.0) {
|
| 119 |
+
let t = i / 4.0;
|
| 120 |
+
let a = t * 5.0 + u.time * 0.6;
|
| 121 |
+
let rad = R * (0.30 + t * 0.42);
|
| 122 |
+
let c = p - vec3<f32>(cos(a) * rad, -R * (1.02 + t * 0.34), sin(a) * rad);
|
| 123 |
+
let seg = length(c) - R * 0.13 * (1.0 - t * 0.45);
|
| 124 |
+
if (seg < d) { d = seg; mat = 1.0; }
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// EYES. Large, flattened, sitting proud of the face β additive, never carved.
|
| 129 |
+
// A concave dimple cannot hold a catchlight, and the catchlight is the whole
|
| 130 |
+
// reason an anime eye reads as alive.
|
| 131 |
+
if (u.eyesOpen > 0.5) {
|
| 132 |
+
// BLINK: two incommensurate phases so the rhythm never reads as a loop.
|
| 133 |
+
// Each spike closes the lids for ~a tenth of its cycle. Sleep holds them
|
| 134 |
+
// three-quarters shut on top; the greet is a happy squint.
|
| 135 |
+
let b1 = abs(fract(T * 0.20) - 0.04);
|
| 136 |
+
let b2 = abs(fract(T * 0.077 + 0.5) - 0.04);
|
| 137 |
+
let blink = (1.0 - smoothstep(0.025, 0.05, min(b1, b2))) * live;
|
| 138 |
+
let eyeY = (1.0 - 0.85 * max(blink, sleepy * 0.75)) * (1.0 - 0.2 * greet);
|
| 139 |
+
let ec = vec3<f32>(R * 0.38, hy - R * 0.22, R * 0.74);
|
| 140 |
+
let white = sdEllip(q - ec, vec3<f32>(R * 0.30, R * 0.38 * eyeY, R * 0.20));
|
| 141 |
+
if (white < d) { d = white; mat = 2.0; }
|
| 142 |
+
// Thinking face: the irises drift up β the classic looking-for-the-word.
|
| 143 |
+
// Otherwise they FOLLOW the pointer: q is mirrored (|x|), so a world-x
|
| 144 |
+
// gaze needs the sign of the eye's own side to keep both looking the
|
| 145 |
+
// same way instead of crossing.
|
| 146 |
+
let lookUp = thinking * R * 0.10;
|
| 147 |
+
let sx = select(-1.0, 1.0, p.x >= 0.0);
|
| 148 |
+
let gz = vec3<f32>(u.gaze.x * sx, -u.gaze.y, 0.0) * R * 0.07 * (1.0 - thinking) * (1.0 - sleepy);
|
| 149 |
+
let iris = sdEllip(q - (ec + vec3<f32>(0.0, -R * 0.04 + lookUp, R * 0.11) + gz),
|
| 150 |
+
vec3<f32>(R * 0.23, R * 0.30 * eyeY, R * 0.17));
|
| 151 |
+
if (iris < d) { d = iris; mat = 3.0; }
|
| 152 |
+
} else {
|
| 153 |
+
// Closed: a lash line, so the face still has a feature.
|
| 154 |
+
let lash = sdEllip(q - vec3<f32>(R * 0.38, hy - R * 0.20, R * 0.80),
|
| 155 |
+
vec3<f32>(R * 0.28, R * 0.05, R * 0.10));
|
| 156 |
+
if (lash < d) { d = lash; mat = 3.0; }
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// The mouth. Anime faces are eyes plus almost nothing, but "almost nothing"
|
| 160 |
+
// is not "nothing" β and it is the one feature that ACTS: while talking the
|
| 161 |
+
// per-token beat opens it (each flap is a real emitted token), while
|
| 162 |
+
// thinking it purses to a dot. Material is a negative sentinel so it can
|
| 163 |
+
// never collide with the sprite id range.
|
| 164 |
+
if (u.eyesOpen > 0.5) {
|
| 165 |
+
// Envelope is the decaying per-token beat (real signal: no tokens, mouth
|
| 166 |
+
// closes); the 12 Hz chatter under it is what makes an open mouth read as
|
| 167 |
+
// SPEAKING rather than surprise. At high token rates beat alone pins the
|
| 168 |
+
// mouth open β the chatter is the flap.
|
| 169 |
+
let open = clamp(u.beat * 1.5, 0.0, 1.0) * talking * (0.30 + 0.70 * abs(sin(u.time * 12.0)));
|
| 170 |
+
var mwB = mix(0.11, 0.055, thinking);
|
| 171 |
+
mwB = mix(mwB, 0.20, greet); // the smile is WIDE
|
| 172 |
+
var mhB = mix(0.055, 0.025, thinking);
|
| 173 |
+
mhB = mix(mhB, 0.020, sleepy); // asleep: barely there
|
| 174 |
+
mhB = mix(mhB, 0.028, greet); // ...and shallow
|
| 175 |
+
let mw = R * mix(mwB, 0.085, open);
|
| 176 |
+
let mh = R * (mhB + 0.11 * open);
|
| 177 |
+
let mo = sdEllip(p - vec3<f32>(0.0, hy - R * 0.62 - open * R * 0.04, R * 0.80),
|
| 178 |
+
vec3<f32>(mw, mh, R * 0.08));
|
| 179 |
+
if (mo < d) { d = mo; mat = -1.0; }
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// Expert sprites in orbit.
|
| 183 |
+
if (u.experts > 0.5) {
|
| 184 |
+
let n = min(u.experts, 24.0);
|
| 185 |
+
let sect = 6.28318 / n;
|
| 186 |
+
let a = atan2(p.z, p.x) + u.time * u.tempo * 0.5;
|
| 187 |
+
let i = round(a / sect);
|
| 188 |
+
let ang = i * sect - u.time * u.tempo * 0.5;
|
| 189 |
+
// Memory mode: the resident fraction keeps the close orbit; the streamed
|
| 190 |
+
// remainder drifts out and shrinks β the pool, drawn honestly.
|
| 191 |
+
let resident = select(1.0, f32((i / n) < u.poolFrac), u.poolFrac > 0.0);
|
| 192 |
+
let orbitR = mix(R * 2.15, R * 1.55, resident);
|
| 193 |
+
let sprR = mix(R * 0.045, R * 0.075, resident);
|
| 194 |
+
let c = p - vec3<f32>(cos(ang) * orbitR, hy + sin(i * 2.0) * R * 0.35, sin(ang) * orbitR);
|
| 195 |
+
let s = length(c) - sprR;
|
| 196 |
+
// Wrap the sector index into [0, n) before it becomes a material id β
|
| 197 |
+
// atan2 makes i negative on half the circle, and 4+i walked into the
|
| 198 |
+
// eye/iris ids there (and past the old mouth id on the other half).
|
| 199 |
+
let id = i - n * floor(i / n);
|
| 200 |
+
if (s < d) { d = s; mat = 4.0 + id; }
|
| 201 |
+
}
|
| 202 |
+
return vec2<f32>(d, mat);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
fn map(p: vec3<f32>) -> f32 { return map2(p).x; }
|
| 206 |
+
|
| 207 |
+
fn normalAt(p: vec3<f32>) -> vec3<f32> {
|
| 208 |
+
let e = vec2<f32>(0.0012, 0.0);
|
| 209 |
+
return normalize(vec3<f32>(
|
| 210 |
+
map(p + e.xyy) - map(p - e.xyy),
|
| 211 |
+
map(p + e.yxy) - map(p - e.yxy),
|
| 212 |
+
map(p + e.yyx) - map(p - e.yyx)));
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// CEL SHADING. Anime light is banded with a hard terminator, not a ramp. Band
|
| 216 |
+
// COUNT comes from the checkpoint's bit width and the split from its quant
|
| 217 |
+
// group, so a coarser build is visibly more posterised.
|
| 218 |
+
fn cel(x: f32, bands: f32, shift: f32) -> f32 {
|
| 219 |
+
let t = clamp(x, 0.0, 1.0);
|
| 220 |
+
let s = floor(t * bands + shift) / bands;
|
| 221 |
+
let f = fract(t * bands + shift);
|
| 222 |
+
return clamp(s + smoothstep(0.46, 0.54, f) / bands, 0.42, 1.0);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
@vertex
|
| 226 |
+
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
|
| 227 |
+
var p = array<vec2<f32>, 3>(vec2<f32>(-1.0, -3.0), vec2<f32>(-1.0, 1.0), vec2<f32>(3.0, 1.0));
|
| 228 |
+
return vec4<f32>(p[i], 0.0, 1.0);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
@fragment
|
| 232 |
+
fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
|
| 233 |
+
let uv = (frag.xy - 0.5 * u.res) / u.res.y;
|
| 234 |
+
let ro = vec3<f32>(0.0, 0.05, 3.05);
|
| 235 |
+
let rd = normalize(vec3<f32>(uv.x, -uv.y, -1.5));
|
| 236 |
+
|
| 237 |
+
var t = 0.0; var hit = false; var mat = 0.0;
|
| 238 |
+
for (var i = 0; i < 92; i = i + 1) {
|
| 239 |
+
let r = map2(ro + rd * t);
|
| 240 |
+
if (r.x < 0.0012) { hit = true; mat = r.y; break; }
|
| 241 |
+
t = t + r.x * 0.85;
|
| 242 |
+
if (t > 6.0) { break; }
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
var shadowA = 0.0;
|
| 246 |
+
if (rd.y < -0.0001) {
|
| 247 |
+
let ft = (-0.92 - ro.y) / rd.y;
|
| 248 |
+
if (ft > 0.0 && (!hit || ft < t)) {
|
| 249 |
+
let fp = ro + rd * ft;
|
| 250 |
+
shadowA = exp(-dot(fp.xz, fp.xz) * 6.0) * 0.42;
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
if (!hit) { return vec4<f32>(0.0, 0.0, 0.0, shadowA); }
|
| 254 |
+
|
| 255 |
+
let p = ro + rd * t;
|
| 256 |
+
let nrm = normalAt(p);
|
| 257 |
+
let key = normalize(vec3<f32>(0.45, 0.70, 0.70));
|
| 258 |
+
let diff = clamp(dot(nrm, key) * 0.5 + 0.5, 0.0, 1.0);
|
| 259 |
+
let lit = cel(diff, u.bands, u.bandShift);
|
| 260 |
+
let fres = pow(1.0 - clamp(dot(nrm, -rd), 0.0, 1.0), 1.8);
|
| 261 |
+
|
| 262 |
+
let SKIN = vec3<f32>(0.98, 0.86, 0.80);
|
| 263 |
+
var col: vec3<f32>;
|
| 264 |
+
|
| 265 |
+
if (mat < -0.5) {
|
| 266 |
+
// Mouth: dark interior, darker the wider it opens.
|
| 267 |
+
col = vec3<f32>(0.42, 0.22, 0.24) * (1.0 - 0.5 * clamp(u.beat, 0.0, 1.0));
|
| 268 |
+
} else if (mat >= 4.0) {
|
| 269 |
+
let id = mat - 4.0;
|
| 270 |
+
let n = min(u.experts, 24.0);
|
| 271 |
+
let m = max(n / max(u.topk, 1.0), 1.0);
|
| 272 |
+
let s = id + floor(u.time * 1.6);
|
| 273 |
+
let hot = select(0.0, 1.0, (s - m * floor(s / m)) < 1.0);
|
| 274 |
+
let fused = select(0.0, 1.0, u.sharedFused > 0.5 && abs(id) < 0.5);
|
| 275 |
+
col = mix(u.hair * 0.45, vec3<f32>(1.0, 0.82, 0.55), max(hot, fused));
|
| 276 |
+
} else if (mat >= 3.0) {
|
| 277 |
+
// Iris: dark base, bright rim low, and one hard catchlight upper-left. The
|
| 278 |
+
// eye must hold both the darkest mass and the brightest pixel in frame.
|
| 279 |
+
let cl = pow(clamp(dot(reflect(-key, nrm), -rd), 0.0, 1.0), 60.0);
|
| 280 |
+
col = u.iris * (0.30 + 0.45 * lit)
|
| 281 |
+
+ u.iris * 1.8 * pow(clamp(-nrm.y, 0.0, 1.0), 2.0) * 0.5
|
| 282 |
+
+ vec3<f32>(1.0) * step(0.55, cl) * 1.6;
|
| 283 |
+
} else if (mat >= 2.0) {
|
| 284 |
+
col = vec3<f32>(0.97, 0.97, 1.0) * (0.82 + 0.18 * lit);
|
| 285 |
+
} else if (mat >= 1.0) {
|
| 286 |
+
col = u.hair * lit;
|
| 287 |
+
} else {
|
| 288 |
+
col = SKIN * lit;
|
| 289 |
+
// A blush band, low on the cheek β cheap, and it is a strong anime cue.
|
| 290 |
+
let cheek = smoothstep(0.30, 0.0, length(vec2<f32>(abs(p.x) - u.headR * 0.52, p.y - u.headR * 0.24)));
|
| 291 |
+
col = mix(col, vec3<f32>(1.0, 0.62, 0.62), cheek * 0.30);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// INK OUTLINE. Weight follows the bit width: a coarser build is drawn with a
|
| 295 |
+
// heavier pen.
|
| 296 |
+
col = mix(col, vec3<f32>(0.04, 0.04, 0.07), smoothstep(1.0 - u.ink, 0.995, fres));
|
| 297 |
+
// ALREADY ON THIS DEVICE. A cached model is lit β it can start now, where an
|
| 298 |
+
// uncached one costs a download first. The line under the card says so in
|
| 299 |
+
// words; this says it before the words are read.
|
| 300 |
+
if (u.cached > 0.5) {
|
| 301 |
+
col = col + u.hair * fres * 0.55 + u.hair * 0.06;
|
| 302 |
+
}
|
| 303 |
+
col = col * (0.94 + 0.14 * u.hover);
|
| 304 |
+
col = pow(clamp(col, vec3<f32>(0.0), vec3<f32>(1.0)), vec3<f32>(1.0 / 2.05));
|
| 305 |
+
return vec4<f32>(col, 1.0);
|
| 306 |
+
}
|
| 307 |
+
`;let z=null;function F(){return z||(z=(async()=>{if(!("gpu"in navigator))return null;try{const e=await navigator.gpu.requestAdapter();return await(e==null?void 0:e.requestDevice())??null}catch{return null}})()),z}const O={dense:[.42,.62,.95],hybrid:[.3,.84,.7],moe:[.96,.6,.36],mla:[.72,.54,.95],embed:[.62,.66,.74]},S={dense:[.3,.52,.92],hybrid:[.16,.72,.6],moe:[.94,.52,.24],mla:[.6,.4,.92],embed:[.45,.5,.58]};function P(e){const i=e.layerKinds.filter(r=>r==="gdn").length/Math.max(e.layers,1),h=e.embeddingOnly===!0?"embed":e.moe!=null?"moe":e.mla!=null?"mla":i>.1?"hybrid":"dense",s=r=>"#"+r.map(t=>Math.round(Math.min(1,t)*255).toString(16).padStart(2,"0")).join(""),n=O[h];return{accent:s(n),accentHi:s(n.map(r=>r*.55+.45))}}function B(e,w=!1,i=0){var b;const s=e.layerKinds.filter(k=>k==="gdn").length/Math.max(e.layers,1),n=e.moe!=null,r=e.mla!=null,t=e.embeddingOnly===!0,d=t?"embed":n?"moe":r?"mla":s>.1?"hybrid":"dense",f=O[d],o=S[d],m=((b=e.moe)==null?void 0:b.bits)??4,p=e.weightFormat==="mlx-safetensors"?64:32,u=parseFloat(T(e).rateLabel.replace(/[^0-9.]/g,"")),g=Number.isFinite(u)?Math.min(1.9,.75+u/160):1,y=Math.max(0,Math.min(1.4,(Math.log2(e.maxContext)-12)/6)),v=new Float32Array(new ArrayBuffer(128));return v.set([0,0,0,0,.3+Math.log2(e.d)*.016,Math.max(4,Math.min(14,Math.round(e.layers/3))),s,e.kvHeads,n?e.moe.experts:0,n?e.moe.topK:0,e.sharedExpertIndex>=0?1:0,m<=3?2:3,m<=3?.46:.34,t?0:1,p===64?.5:0,0,f[0],f[1],f[2],0,o[0],o[1],o[2],w?1:0,g,0,y,i,0,0,0,0]),v}async function N(e,w){const i=await F(),h=i?e.getContext("webgpu"):null;if(!i||!h)return null;const s=navigator.gpu.getPreferredCanvasFormat();h.configure({device:i,format:s,alphaMode:"premultiplied"});const n=i.createShaderModule({code:C}),r=i.createRenderPipeline({layout:"auto",vertex:{module:n,entryPoint:"vs"},fragment:{module:n,entryPoint:"fs",targets:[{format:s,blend:{color:{srcFactor:"one",dstFactor:"one-minus-src-alpha"},alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha"}}}]}});let t=B(w);const d=i.createBuffer({size:t.byteLength,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),f=i.createBindGroup({layout:r.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:d}}]}),o=matchMedia("(prefers-reduced-motion: reduce)").matches;let m=0,p=0,u=0,g=!1,y=0,v=0,b=0,k=0,E=!1;const A=()=>{const a=Math.min(devicePixelRatio||1,2),l=Math.max(1,Math.round(e.clientWidth*a)),R=Math.max(1,Math.round(e.clientHeight*a));(e.width!==l||e.height!==R)&&(e.width=l,e.height=R),t[0]=e.width,t[1]=e.height,t[2]=p,t[3]=m,t[15]=v,t[25]=y,t[28]=b,t[29]=k,y*=.9,i.queue.writeBuffer(d,0,t);const x=i.createCommandEncoder(),M=x.beginRenderPass({colorAttachments:[{view:h.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:0},loadOp:"clear",storeOp:"store"}]});M.setPipeline(r),M.setBindGroup(0,f),M.draw(3),M.end(),i.queue.submit([x.finish()])},c=()=>{g||(E||A(),o||(p+=1/60,u=requestAnimationFrame(c)))},I=()=>{o&&!g&&c()};return window.addEventListener("resize",I),c(),{pulse(){y=1},setSpec(a,l=!1,R=0){const x=B(a,l,R);x[2]=p,t=x,o&&c()},setHover(a){m=a?1:0},setMood(a){v=a==="talking"?2:a==="thinking"?1:a==="sleepy"?3:a==="greet"?4:0,o&&c()},setGaze(a,l){b=Math.max(-1,Math.min(1,a)),k=Math.max(-1,Math.min(1,l)),o&&c()},async snapshot(a=!1){a&&(E=!0,t[30]=1,A()),await i.queue.onSubmittedWorkDone();const l=e.toDataURL("image/png");return a&&(t[30]=0,E=!1),l},destroy(){g=!0,cancelAnimationFrame(u),window.removeEventListener("resize",I),d.destroy()}}}export{N as a,P as m};
|
assets/model-registry-BoK-ObgACo.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))n(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const m of i.addedNodes)m.tagName==="LINK"&&m.rel==="modulepreload"&&n(m)}).observe(document,{childList:!0,subtree:!0});function a(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(o){if(o.ep)return;o.ep=!0;const i=a(o);fetch(o.href,i)}})();function g(e){const t=`${e}model.`;return{embedWeight:[`${t}embed_tokens.weight`],embedScale:[`${t}embed_tokens.scales`],lmHeadWeight:[`${e}lm_head.weight`],lmHeadScale:[`${e}lm_head.scales`],finalNorm:[`${t}norm.weight`],biasFor:a=>a.replace(/\.weight$/,".biases"),layer:a=>{const n=`${t}layers.${a}`,o=`${n}.linear_attn`;return{qkvWeight:[`${n}.self_attn.q_proj.weight`],qkvScale:[`${n}.self_attn.q_proj.scales`],oProjWeight:[`${n}.self_attn.o_proj.weight`],oProjScale:[`${n}.self_attn.o_proj.scales`],norm1:[`${n}.input_layernorm.weight`],norm2:[`${n}.post_attention_layernorm.weight`],ffnWeight:[`${n}.mlp.gate_proj.weight`],ffnScale:[`${n}.mlp.gate_proj.scales`],ffnDownWeight:[`${n}.mlp.down_proj.weight`],ffnDownScale:[`${n}.mlp.down_proj.scales`],qNorm:[`${n}.self_attn.q_norm.weight`],kNorm:[`${n}.self_attn.k_norm.weight`],gdnQkvWeight:[`${o}.in_proj_qkv.weight`],gdnQkvScale:[`${o}.in_proj_qkv.scales`],gdnZWeight:[`${o}.in_proj_z.weight`],gdnZScale:[`${o}.in_proj_z.scales`],gdnAWeight:[`${o}.in_proj_a.weight`],gdnAScale:[`${o}.in_proj_a.scales`],gdnBWeight:[`${o}.in_proj_b.weight`],gdnBScale:[`${o}.in_proj_b.scales`],gdnALog:[`${o}.A_log`],gdnDtBias:[`${o}.dt_bias`],gdnConvWeight:[`${o}.conv1d.weight`],gdnNormWeight:[`${o}.norm.weight`],gdnOutWeight:[`${o}.out_proj.weight`],gdnOutScale:[`${o}.out_proj.scales`]}}}}function l(e){var k,v;if(e.heads%e.kvHeads!==0)throw new Error(`${e.id}: heads not divisible by kvHeads`);if(e.headDim%2!==0)throw new Error(`${e.id}: headDim must be even (RoPE pairs)`);if(e.d%32!==0)throw new Error(`${e.id}: d must be divisible by 32 (int4 scale groups)`);if(e.fullAttnInterval&&!e.gdn)throw new Error(`${e.id}: fullAttnInterval requires gdn dims`);const t=e.heads*e.headDim,a=e.kvHeads*e.headDim,n=e.headDim/4,o=e.gdn??{kHeads:e.heads,vHeads:e.heads,headK:e.headDim,headV:e.headDim,convK:4};if(o.vHeads%o.kHeads!==0)throw new Error(`${e.id}: gdn vHeads not divisible by kHeads`);const i=e.fullAttnInterval??0,m=Object.freeze(Array.from({length:e.layers},(p,u)=>i&&(u+1)%i!==0?"gdn":"attn")),s=new Set(((k=e.moe)==null?void 0:k.denseLayers)??[]);if(s.size&&((v=e.moe)==null?void 0:v.denseFfn)===void 0)throw new Error(`${e.id}: moe.denseLayers needs moe.denseFfn β a dense layer's width is not moe_intermediate_size`);const d=Object.freeze(Array.from({length:e.layers},(p,u)=>e.moe&&!s.has(u)?"moe":"dense")),c=m.filter(p=>p==="attn").length,r=e.mla;if(r){if(r.vHeadDim!==e.headDim)throw new Error(`${e.id}: mla.vHeadDim ${r.vHeadDim} must equal headDim ${e.headDim} (o_proj contracts over heads*vHeadDim, and qDim is derived from headDim)`);if(r.qLoraRank!==null)throw new Error(`${e.id}: mla.qLoraRank ${r.qLoraRank} β a q_a_proj/q_b_proj split checkpoint (V2-full, V3). The loader plans a single q_proj; nothing here produces those records.`)}const h=r?r.kvLoraRank+r.qkRopeHeadDim:0,w=r?e.maxPages*e.pageSize*r.kvLoraRank*2:0;if(r&&w%256!==0)throw new Error(`${e.id}: MLA latent region is ${w} B, not a multiple of 256`);const q=e.mla?e.mla.qkRopeHeadDim:Math.round(e.headDim*(e.partialRotaryFactor??1));if(q%2!==0)throw new Error(`${e.id}: rotaryDim must be even (RoPE pairs)`);return Object.freeze({...e,layerKinds:m,ffnKinds:d,ffnWidthAt:p=>d[p]==="dense"&&e.moe?e.moe.denseFfn:e.ffn,rotaryDim:q,halfRotary:q/2,cAttnDim:t+2*a+(e.attnGate?t:0),gdnKHeads:o.kHeads,gdnVHeads:o.vHeads,gdnHeadK:o.headK,gdnHeadV:o.headV,gdnConvK:o.convK,gdnGvaGroup:o.vHeads/o.kHeads,gdnKDim:o.kHeads*o.headK,gdnVDim:o.vHeads*o.headV,gdnQkvDim:2*o.kHeads*o.headK+o.vHeads*o.headV,gdnStatePerHead:o.headK*o.headV,gdnProjRows:2*o.kHeads*o.headK+o.vHeads*o.headV+o.vHeads*o.headV+2*o.vHeads,qDim:t,kvDim:a,qkvDim:t+2*a,gqaGroup:e.heads/e.kvHeads,halfHeadDim:e.headDim/2,dPacked:e.d/8,dScales:e.d/32,qkvGroupPairs:e.heads*(e.headDim/2),qkvPairs:(t+2*a)/2,headPageStride:e.pageSize*e.headDim,kvPageStride:2*e.kvHeads*e.pageSize*e.headDim,vPageOffset:e.kvHeads*e.pageSize*e.headDim,maxContext:e.maxPages*e.pageSize,kvBytesPerToken:r?h*2*c:2*e.kvHeads*e.headDim*2*c,mlaCachePerToken:h,mlaQProjRows:r?e.heads*(r.qkNopeHeadDim+r.qkRopeHeadDim):0,mlaKvaRows:r?r.kvLoraRank+r.qkRopeHeadDim:0,mlaKvbRows:r?e.heads*(r.qkNopeHeadDim+r.vHeadDim):0,mlaLatentBytes:w,mlaCacheBytes:r?e.maxPages*e.pageSize*h*2:0,kvI8RowWords:n,kvI8SlotWords:2*n,kvI8HeadWords:e.pageSize*2*n,kvI8PageWords:e.kvHeads*e.pageSize*2*n,kvScalesPerSlot:2,kvScalesPerHead:e.pageSize*2,kvScalesPerPage:e.kvHeads*e.pageSize*2,moeSlots:e.moe?e.moe.topK+(e.moe.sharedExpert??!0?1:0):1,sharedExpertIndex:e.moe&&(e.moe.sharedExpert??!0)?e.moe.experts:-1})}function L(e){const t=e.ropeScaling;if(!t||t.ropeType!=="yarn"||!t.mscaleAllDim||t.factor<=1)return 1;const a=.1*t.mscaleAllDim*Math.log(t.factor)+1;return a*a}function E(e){const t=new Float32Array(e.halfRotary),a=e.ropeScaling;if((a==null?void 0:a.ropeType)==="yarn"){const n=e.rotaryDim,o=s=>n*Math.log(a.originalMaxPositionEmbeddings/(s*2*Math.PI))/(2*Math.log(e.ropeTheta)),i=Math.max(Math.floor(o(a.betaFast)),0);let m=Math.min(Math.ceil(o(a.betaSlow)),n-1);m===i&&(m=i+.001);for(let s=0;s<e.halfRotary;s++){const d=Math.pow(e.ropeTheta,-(2*s)/n),c=d/a.factor,h=1-Math.min(Math.max((s-i)/(m-i),0),1);t[s]=c*(1-h)+d*h}return t}for(let n=0;n<e.halfRotary;n++){let o=Math.pow(e.ropeTheta,-(2*n)/e.rotaryDim);if(a){const i=a.originalMaxPositionEmbeddings/a.lowFreqFactor,m=a.originalMaxPositionEmbeddings/a.highFreqFactor,s=2*Math.PI/o;if(s>i)o=o/a.factor;else if(!(s<m)){const d=(a.originalMaxPositionEmbeddings/s-a.lowFreqFactor)/(a.highFreqFactor-a.lowFreqFactor);o=(1-d)*(o/a.factor)+d*o}}t[n]=o}return t}const _=l({id:"phi3-mini",d:3072,layers:32,heads:32,kvHeads:32,headDim:96,ffn:8192,vocab:32064,pageSize:16,maxPages:257,maxSeq:4096,ropeTheta:1e4,rmsEps:1e-5,tiedEmbeddings:!1,qkNorm:!1,stops:[2,32e3,32007],chatTemplateId:"phi3",tokenizerKind:"spm",hfRepo:"mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC",paramNaming:{embedWeight:["transformer.embd.q_weight","embed_tokens.q_weight","model.embed_tokens.q_weight"],embedScale:["transformer.embd.q_scale","embed_tokens.q_scale","model.embed_tokens.q_scale"],lmHeadWeight:["lm_head.q_weight","model.lm_head.q_weight"],lmHeadScale:["lm_head.q_scale","model.lm_head.q_scale"],finalNorm:["transformer.norm.weight","model.norm.weight","norm.weight"],layer:e=>{const t=`transformer.h.${e}`,a=`model.layers.${e}`;return{qkvWeight:[`${t}.mixer.qkv_proj.q_weight`,`${a}.self_attn.qkv_proj.q_weight`],qkvScale:[`${t}.mixer.qkv_proj.q_scale`,`${a}.self_attn.qkv_proj.q_scale`],oProjWeight:[`${t}.mixer.out_proj.q_weight`,`${a}.self_attn.o_proj.q_weight`],oProjScale:[`${t}.mixer.out_proj.q_scale`,`${a}.self_attn.o_proj.q_scale`],norm1:[`${t}.ln.weight`,`${a}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`,`${a}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.gate_up_proj.q_weight`,`${a}.mlp.gate_up_proj.q_weight`],ffnScale:[`${t}.mlp.gate_up_proj.q_scale`,`${a}.mlp.gate_up_proj.q_scale`],ffnDownWeight:[`${t}.mlp.down_proj.q_weight`,`${a}.mlp.down_proj.q_weight`],ffnDownScale:[`${t}.mlp.down_proj.q_scale`,`${a}.mlp.down_proj.q_scale`]}}}}),y=l({id:"qwen3-4b",d:2560,layers:36,heads:32,kvHeads:8,headDim:128,ffn:9728,vocab:151936,pageSize:16,maxPages:448,maxSeq:40960,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[151645,151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlc-ai/Qwen3-4B-q4f16_1-MLC",paramNaming:{embedWeight:["model.embed_tokens.q_weight"],embedScale:["model.embed_tokens.q_scale"],lmHeadWeight:["model.embed_tokens.q_weight"],lmHeadScale:["model.embed_tokens.q_scale"],finalNorm:["model.norm.weight"],layer:e=>{const t=`model.layers.${e}`;return{qkvWeight:[`${t}.self_attn.c_attn.q_weight`,`${t}.self_attn.qkv_proj.q_weight`],qkvScale:[`${t}.self_attn.c_attn.q_scale`,`${t}.self_attn.qkv_proj.q_scale`],oProjWeight:[`${t}.self_attn.o_proj.q_weight`],oProjScale:[`${t}.self_attn.o_proj.q_scale`],norm1:[`${t}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.gate_up_proj.q_weight`],ffnScale:[`${t}.mlp.gate_up_proj.q_scale`],ffnDownWeight:[`${t}.mlp.down_proj.q_weight`],ffnDownScale:[`${t}.mlp.down_proj.q_scale`],qNorm:[`${t}.self_attn.q_norm.weight`],kNorm:[`${t}.self_attn.k_norm.weight`]}}}}),j=l({id:"qwen35-4b",d:2560,layers:32,heads:16,kvHeads:4,headDim:256,ffn:9216,vocab:248320,pageSize:16,maxPages:2048,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlc-ai/Qwen3.5-4B-q4f16_1-MLC",manifestName:"tensor-cache.json",fullAttnInterval:4,gdn:{kHeads:16,vHeads:32,headK:128,headV:128,convK:4},partialRotaryFactor:.25,attnGate:!0,paramNaming:{embedWeight:["model.embed_tokens.q_weight"],embedScale:["model.embed_tokens.q_scale"],lmHeadWeight:["model.embed_tokens.q_weight"],lmHeadScale:["model.embed_tokens.q_scale"],finalNorm:["model.norm.weight"],layer:e=>{const t=`model.layers.${e}`,a=`${t}.linear_attn`;return{qkvWeight:[`${t}.self_attn.c_attn.q_weight`],qkvScale:[`${t}.self_attn.c_attn.q_scale`],oProjWeight:[`${t}.self_attn.o_proj.q_weight`],oProjScale:[`${t}.self_attn.o_proj.q_scale`],norm1:[`${t}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.gate_up_proj.q_weight`],ffnScale:[`${t}.mlp.gate_up_proj.q_scale`],ffnDownWeight:[`${t}.mlp.down_proj.q_weight`],ffnDownScale:[`${t}.mlp.down_proj.q_scale`],qNorm:[`${t}.self_attn.q_norm.weight`],kNorm:[`${t}.self_attn.k_norm.weight`],gdnQkvWeight:[`${a}.in_proj_qkv.q_weight`],gdnQkvScale:[`${a}.in_proj_qkv.q_scale`],gdnZWeight:[`${a}.in_proj_z.q_weight`],gdnZScale:[`${a}.in_proj_z.q_scale`],gdnAWeight:[`${a}.in_proj_a.q_weight`],gdnAScale:[`${a}.in_proj_a.q_scale`],gdnBWeight:[`${a}.in_proj_b.q_weight`],gdnBScale:[`${a}.in_proj_b.q_scale`],gdnALog:[`${a}.A_log`],gdnDtBias:[`${a}.dt_bias`],gdnConvWeight:[`${a}.conv1d_weight`],gdnNormWeight:[`${a}.norm.weight`],gdnOutWeight:[`${a}.out_proj.q_weight`],gdnOutScale:[`${a}.out_proj.q_scale`]}}}}),f=l({id:"qwen36-35b-a3b",d:2048,layers:40,heads:16,kvHeads:2,headDim:256,ffn:512,vocab:248320,pageSize:16,maxPages:2048,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"lmstudio-community/Qwen3.6-35B-A3B-MLX-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mlxPrefix:"language_model.",fullAttnInterval:4,gdn:{kHeads:16,vHeads:32,headK:128,headV:128,convK:4},partialRotaryFactor:.25,attnGate:!0,moe:{experts:256,topK:8,normTopkProb:!0},paramNaming:{embedWeight:["language_model.model.embed_tokens.weight"],embedScale:["language_model.model.embed_tokens.scales"],lmHeadWeight:["language_model.lm_head.weight"],lmHeadScale:["language_model.lm_head.scales"],finalNorm:["language_model.model.norm.weight"],biasFor:e=>e.replace(/\.weight$/,".biases"),layer:e=>{const t=`language_model.model.layers.${e}`,a=`${t}.linear_attn`;return{qkvWeight:[`${t}.self_attn.q_proj.weight`],qkvScale:[`${t}.self_attn.q_proj.scales`],oProjWeight:[`${t}.self_attn.o_proj.weight`],oProjScale:[`${t}.self_attn.o_proj.scales`],norm1:[`${t}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.switch_mlp.gate_proj.weight`],ffnScale:[`${t}.mlp.switch_mlp.gate_proj.scales`],ffnDownWeight:[`${t}.mlp.switch_mlp.down_proj.weight`],ffnDownScale:[`${t}.mlp.switch_mlp.down_proj.scales`],qNorm:[`${t}.self_attn.q_norm.weight`],kNorm:[`${t}.self_attn.k_norm.weight`],gdnQkvWeight:[`${a}.in_proj_qkv.weight`],gdnQkvScale:[`${a}.in_proj_qkv.scales`],gdnZWeight:[`${a}.in_proj_z.weight`],gdnZScale:[`${a}.in_proj_z.scales`],gdnAWeight:[`${a}.in_proj_a.weight`],gdnAScale:[`${a}.in_proj_a.scales`],gdnBWeight:[`${a}.in_proj_b.weight`],gdnBScale:[`${a}.in_proj_b.scales`],gdnALog:[`${a}.A_log`],gdnDtBias:[`${a}.dt_bias`],gdnConvWeight:[`${a}.conv1d.weight`],gdnNormWeight:[`${a}.norm.weight`],gdnOutWeight:[`${a}.out_proj.weight`],gdnOutScale:[`${a}.out_proj.scales`]}}}}),B=l({...f,id:"qwen36-35b-a3b-q3",hfRepo:"abgunaydin/Qwen3.6-35B-A3B-MLX-q3exp",moe:{...f.moe,bits:3}}),b=l({id:"qwen3-8-27b-4bit",d:5120,layers:64,heads:24,kvHeads:4,headDim:256,ffn:17408,vocab:248320,pageSize:16,maxPages:1024,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3.8-27B-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mlxPrefix:"language_model.",fullAttnInterval:4,gdn:{kHeads:16,vHeads:48,headK:128,headV:128,convK:4},attnGate:!0,partialRotaryFactor:.25,paramNaming:g("language_model.")}),x=l({id:"qwen3-5-9b-mlx-4bit",d:4096,layers:32,heads:16,kvHeads:4,headDim:256,ffn:12288,vocab:248320,pageSize:16,maxPages:2048,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"lmstudio-community/Qwen3.5-9B-MLX-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mlxPrefix:"language_model.",fullAttnInterval:4,gdn:{kHeads:16,vHeads:32,headK:128,headV:128,convK:4},attnGate:!0,partialRotaryFactor:.25,paramNaming:g("language_model.")}),D=l({id:"qwen3-embedding-0-6b-4bit-dwq",d:1024,layers:28,heads:16,kvHeads:8,headDim:128,ffn:3072,vocab:151669,pageSize:16,maxPages:512,maxSeq:32768,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",embeddingOnly:!0,paramNaming:g("")});l({id:"deepseek-v2-lite-chat-4bit",d:2048,layers:27,heads:16,kvHeads:16,headDim:128,ffn:1408,vocab:102400,pageSize:16,maxPages:2112,maxSeq:163840,ropeTheta:1e4,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!1,stops:[100001],chatTemplateId:"deepseek",tokenizerKind:"byteLevel",hfRepo:"mlx-community/DeepSeek-V2-Lite-Chat-4bit-mlx",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mla:{kvLoraRank:512,qLoraRank:null,qkNopeHeadDim:128,qkRopeHeadDim:64,vHeadDim:128},moe:{experts:64,topK:6,normTopkProb:!1,sharedExpert:!0,routerBits:16,denseLayers:[0],denseFfn:10944},ropeScaling:{ropeType:"yarn",factor:40,betaFast:32,betaSlow:1,originalMaxPositionEmbeddings:4096,mscale:.707,mscaleAllDim:.707},paramNaming:g("")});const H=l({id:"qwen3-30b-a3b-4bit",d:2048,layers:48,heads:32,kvHeads:4,headDim:128,ffn:768,vocab:151936,pageSize:16,maxPages:640,maxSeq:40960,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[151645,151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3-30B-A3B-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",moe:{experts:128,topK:8,normTopkProb:!0,sharedExpert:!1,routerBits:4},paramNaming:g("")}),N=l({id:"llama-3-2-1b-instruct-4bit",d:2048,layers:16,heads:32,kvHeads:8,headDim:64,ffn:8192,vocab:128256,pageSize:16,maxPages:2048,maxSeq:131072,ropeTheta:5e5,ropeScaling:{ropeType:"llama3",factor:32,lowFreqFactor:1,highFreqFactor:4,originalMaxPositionEmbeddings:8192},rmsEps:1e-5,tiedEmbeddings:!0,qkNorm:!1,stops:[128009,128001],chatTemplateId:"llama3",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Llama-3.2-1B-Instruct-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",paramNaming:g("")}),P=l({id:"qwen3-4b-mlx",d:2560,layers:36,heads:32,kvHeads:8,headDim:128,ffn:9728,vocab:151936,pageSize:16,maxPages:448,maxSeq:40960,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[151645,151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3-4B-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",paramNaming:g("")}),W=[{param:"",spec:_},{param:"qwen3",spec:y},{param:"qwen35",spec:j},{param:"qwen36q3",spec:B},{param:"qwen36",spec:f},{param:"qwen3mlx",spec:P},{param:"llama32",spec:N},{param:"qwen30b",spec:H},{param:"embed",spec:D},{param:"qwen35mlx",spec:x},{param:"qwen38",spec:b}];function z(e){const t=e&&W.find(a=>a.param===e);return t?t.spec:_}function M(e,t){if(!Number.isFinite(t)||t<=0)return e;const a=Math.max(1,Math.min(Math.ceil(t/e.pageSize),Math.floor(e.maxSeq/e.pageSize)));return a===e.maxPages?e:l({...e,maxPages:a})}const $="zero-tvm-weights";function A(e){return e.id===_.id?$:`${$}.${e.id}`}const S={[B.id]:{name:"Qwen3.6-35B-A3B",params:"35B-A3B MoE Β· 3-bit experts",sizeLabel:"~16.4 GB",rateLabel:"~66 t/s",poolModes:[{slots:0,label:"Full Β· 15.7 GB resident Β· ~66 t/s"},{slots:128,label:"Half Β· ~8.4 GB resident Β· ~15 t/s",note:"pooled rates are the 2026-08-15 AC round, whose own unpooled control read 58.6 t/s β compare against that, not the 66 above"},{slots:64,label:"Quarter Β· ~4.8 GB resident Β· ~12 t/s",note:"long prompts run token-by-token"}],ramNote:"needs ~20 GB free RAM"},[f.id]:{name:"Qwen3.6-35B-A3B",params:"35B-A3B MoE Β· full 4-bit",sizeLabel:"~19.5 GB",poolModes:[{slots:0,label:"Full Β· 19.7 GB resident"},{slots:128,label:"Half Β· ~11 GB est"},{slots:64,label:"Quarter Β· ~6 GB est",note:"long prompts run token-by-token"}],rateLabel:"",ramNote:"needs ~24 GB free RAM (64 GB Mac recommended)"},[j.id]:{name:"Qwen3.5-4B",params:"4B hybrid (DeltaNet)",sizeLabel:"~2.6 GB",rateLabel:"~65 t/s"},[y.id]:{name:"Qwen3-4B",params:"4B dense Β· q4f16_1",sizeLabel:"~2.3 GB",rateLabel:"~60 t/s"},[_.id]:{name:"Phi-3-mini",params:"3.8B dense",sizeLabel:"~2 GB",rateLabel:"~70 t/s"},[P.id]:{name:"Qwen3-4B",params:"4B dense Β· MLX 4-bit",sizeLabel:"~2.1 GB",rateLabel:"~81 t/s"},[N.id]:{name:"Llama-3.2-1B-Instruct",params:"1B dense",sizeLabel:"~0.6 GB",rateLabel:"~256 t/s"},[H.id]:{name:"Qwen3-30B-A3B",params:"30B-A3B MoE",sizeLabel:"~17.2 GB",rateLabel:"~75 t/s",ramNote:"needs ~20 GB free RAM",poolModes:[{slots:0,label:"Full Β· ~17 GB Β· ~75 t/s"},{slots:96,label:"ΒΎ Β· ~13 GB est"},{slots:64,label:"Half Β· ~9.5 GB est",note:"long prompts run token-by-token"}]},[D.id]:{name:"Qwen3-Embedding-0.6B",params:"0.6B embedding Β· last-token pooled",sizeLabel:"~0.35 GB",rateLabel:""},[x.id]:{name:"Qwen3.5-9B",params:"9B hybrid (DeltaNet)",sizeLabel:"~4.7 GB",rateLabel:"~43 t/s"},[b.id]:{name:"Qwen3.8-27B",params:"27B hybrid (DeltaNet)",sizeLabel:"~14.1 GB",rateLabel:"",ramNote:"needs ~18 GB free RAM"}};function R(e){return S[e.id]??S[_.id]}export{_ as P,W as S,$ as W,z as a,L as b,R as m,A as o,E as r,M as s};
|
assets/model-select-DhhV9mCi0T.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{s as D,a as B}from"./model-registry-BoK-ObgACo.js";import{S as pt,m as mt}from"./model-registry-BoK-ObgACo.js";import{P as O,r as F}from"./weight-loader-ByOLMd9f5C.js";async function j(e){try{const c=await caches.keys();for(const t of c){const r=await(await caches.open(t)).match(e);if(r)return r.text()}}catch{}const n=await fetch(e);if(!n.ok)throw new Error(`HTTP ${n.status} fetching tokenizer.json`);return n.text()}const I="β",U=new RegExp(I,"g"),H=/^<0x([0-9A-Fa-f]{2})>$/,J=new TextEncoder;function W(e){const n=e.model.vocab,c=e.model.merges,t=new Array(Math.max(...Object.values(n))+1);for(const[i,f]of Object.entries(n))t[f]=i;for(const i of e.added_tokens??[])t[i.id]=i.content;const d=new Map;for(let i=0;i<c.length;i++)d.set(c[i],i);const r=new Map,a=new Set;for(const i of e.added_tokens??[])r.set(i.content,{id:i.id,lstrip:i.lstrip,rstrip:i.rstrip}),a.add(i.id);const m=[...r.keys()].sort((i,f)=>f.length-i.length).map(i=>i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|"),$=m?new RegExp(`(${m})`):null,E=n["<s>"]??1,T=n["</s>"]??2,y=n["<unk>"]??0,b=new Int32Array(256).fill(y);for(let i=0;i<256;i++){const f=n[`<0x${i.toString(16).toUpperCase().padStart(2,"0")}>`];f!==void 0&&(b[i]=f)}const _=new Map,x=new Set;for(let i=0;i<t.length;i++){const f=t[i];if(!f)continue;const u=H.exec(f);if(u){_.set(i,parseInt(u[1],16));continue}if(f==="<s>"||f==="</s>"||f==="<pad>"){x.add(i);continue}f.startsWith("<|")&&f.endsWith("|>")&&x.add(i)}function A(i){if(i.length<=1)return i;for(;;){let f=1/0,u=-1;for(let h=0;h<i.length-1;h++){const o=i[h]+" "+i[h+1],s=d.get(o);s!==void 0&&s<f&&(f=s,u=h)}if(u===-1)break;const k=i[u]+i[u+1];i=[...i.slice(0,u),k,...i.slice(u+2)]}return i}const M=n[I];function C(i){if(i.length===0)return[];const f=I+i.replaceAll(I," "),u=$?f.split($):[f];for(let h=1;h<u.length;h+=2){const o=r.get(u[h]);o&&(o.lstrip&&h>0&&(u[h-1]=u[h-1].trimEnd()),o.rstrip&&h<u.length-1&&(u[h+1]=u[h+1].trimStart()))}const k=[];for(let h=0;h<u.length;h++){const o=u[h];if(!o)continue;if(h%2===1){const l=r.get(o);if(l!==void 0){k.push(l.id);continue}}const s=o.replaceAll(" ",I);for(const l of A([...s])){const p=n[l];if(p!==void 0)k.push(p);else for(const g of J.encode(l))k.push(b[g])}}return k.length>1&&k[0]===M&&a.has(k[1])&&k.shift(),k}const L=new TextDecoder("utf-8",{fatal:!1});function R(i){let f="",u=[];const k=()=>{u.length!==0&&(f+=L.decode(Uint8Array.from(u)),u=[])};for(const h of i){if(h<0||x.has(h)){k();continue}const o=_.get(h);if(o!==void 0){u.push(o);continue}const s=t[h];s&&(k(),f+=s)}return k(),f=f.replace(U," "),f.startsWith(" ")?f.slice(1):f}return{encode:C,decode:R,bosId:E,eosId:T}}async function K(e,n=O){e==null||e("Loading tokenizer.json...");const c=n+"tokenizer.json",t=JSON.parse(await j(c)),d=W(t);return e==null||e("Tokenizer ready"),d}function q(e,n){let c="";for(const t of e)t.role==="system"?c+=`<|system|>
|
| 2 |
+
${t.content}<|end|>
|
| 3 |
+
`:t.role==="user"?c+=`<|user|>
|
| 4 |
+
${t.content}<|end|>
|
| 5 |
+
`:c+=`<|assistant|>
|
| 6 |
+
${t.content}<|end|>
|
| 7 |
+
`;return c+=`<|assistant|>
|
| 8 |
+
`,n.encode(c)}const Q="https://huggingface.co/mlc-ai/Qwen3-4B-q4f16_1-MLC/resolve/main/";async function V(e){try{const c=await caches.keys();for(const t of c){const r=await(await caches.open(t)).match(e);if(r)return r.text()}}catch{}const n=await fetch(e);if(!n.ok)throw new Error(`HTTP ${n.status} fetching tokenizer.json`);return n.text()}const N=new RegExp("'(?:[sS]|[tT]|[rR][eE]|[vV][eE]|[mM]|[lL][lL]|[dD])|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+","gu");function Y(e){var r,a;const n=e.pre_tokenizer,t=(a=(r=((n==null?void 0:n.pretokenizers)??(n?[n]:[])).find(m=>m.type==="Split"))==null?void 0:r.pattern)==null?void 0:a.Regex;if(typeof t!="string")return N;const d=t.replace("(?i:'s|'t|'re|'ve|'m|'ll|'d)","'(?:[sS]|[tT]|[rR][eE]|[vV][eE]|[mM]|[lL][lL]|[dD])");try{return new RegExp(d,"gu")}catch{return N}}function P(){const e=new Array(256),n=new Map;let c=0;for(let t=0;t<256;t++){const d=t>=33&&t<=126||t>=161&&t<=172||t>=174&&t<=255,r=String.fromCharCode(d?t:256+c++);e[t]=r,n.set(r,t)}return{byteToChar:e,charToByte:n}}const Z=new TextEncoder;function G(e){const n=e.model.vocab,{byteToChar:c,charToByte:t}=P(),d=Y(e),r=e.normalizer===void 0||e.normalizer!==null;let a=0;for(const o of Object.values(n))o>a&&(a=o);const m=new Array(a+1);for(const[o,s]of Object.entries(n))m[s]=o;const $=new Map,E=e.model.merges;for(let o=0;o<E.length;o++){const s=E[o];$.set(typeof s=="string"?s:s[0]+" "+s[1],o)}const T=new Map,y=new Set,b=new Map;for(const o of e.added_tokens??[])T.set(o.content,o.id),o.special?y.add(o.id):b.set(o.id,o.content);const _=[...T.keys()].sort((o,s)=>s.length-o.length).map(o=>o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|"),x=_?new RegExp(`(${_})`):null,A=T.get("<|im_end|>")??T.get("<|eot_id|>")??151645,M=T.get("<|endoftext|>")??T.get("<|begin_of_text|>")??151643,C=[A,T.get("<|end_of_text|>")??M],L=new Map;function R(o){const s=L.get(o);if(s!==void 0)return s;let l=[...o];for(;l.length>1;){let p=1/0,g=-1;for(let w=0;w<l.length-1;w++){const v=$.get(l[w]+" "+l[w+1]);v!==void 0&&v<p&&(p=v,g=w)}if(g===-1)break;const S=l[g]+l[g+1];l=[...l.slice(0,g),S,...l.slice(g+2)]}return L.set(o,l),l}function i(o){const s=[];let l=0;for(const p of o.matchAll(d))p.index>l&&s.push(o.slice(l,p.index)),s.push(p[0]),l=p.index+p[0].length;return l<o.length&&s.push(o.slice(l)),s}function f(o){if(o.length===0)return[];const s=x?o.split(x):[o],l=[];for(let p=0;p<s.length;p++){const g=s[p];if(g){if(p%2===1){const S=T.get(g);if(S!==void 0){l.push(S);continue}}for(const S of i(r?g.normalize("NFC"):g)){let w="";for(const v of Z.encode(S))w+=c[v];for(const v of R(w)){const z=n[v];z!==void 0&&l.push(z)}}}}return l}const u=new Array(m.length).fill(null);for(let o=0;o<m.length;o++){const s=m[o];if(!s)continue;const l=new Uint8Array(s.length);for(let p=0;p<s.length;p++)l[p]=t.get(s[p])??0;u[o]=l}const k=new TextDecoder("utf-8",{fatal:!1});function h(o){let s="",l=[];const p=()=>{l.length!==0&&(s+=k.decode(Uint8Array.from(l)),l=[])};for(const g of o){if(g<0||y.has(g)){p();continue}const S=b.get(g);if(S!==void 0){p(),s+=S;continue}const w=u[g];if(w)for(const v of w)l.push(v)}return p(),s}return{encode:f,decode:h,bosId:M,eosId:A,stopIds:C}}async function X(e,n=Q){e==null||e("Loading tokenizer.json...");const c=n+"tokenizer.json",t=JSON.parse(await V(c)),d=G(t);return e==null||e("Tokenizer ready"),d}function tt(e,n,c){let t="";for(const d of e){const r=d.role==="assistant"?`<think>
|
| 9 |
+
|
| 10 |
+
</think>
|
| 11 |
+
|
| 12 |
+
${d.content}`:d.content;t+=`<|im_start|>${d.role}
|
| 13 |
+
${r}<|im_end|>
|
| 14 |
+
`}return t+=`<|im_start|>assistant
|
| 15 |
+
`,t+=`<think>
|
| 16 |
+
|
| 17 |
+
</think>
|
| 18 |
+
|
| 19 |
+
`,n.encode(t)}const et=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function nt(e=new Date){return`${String(e.getDate()).padStart(2,"0")} ${et[e.getMonth()]} ${e.getFullYear()}`}function ot(e,n,c){const t="<ο½beginβofβsentenceο½>",d="<ο½endβofβsentenceο½>";let r=t;for(const a of e)a.role==="user"?r+=`User: ${a.content}
|
| 20 |
+
|
| 21 |
+
`:a.role==="assistant"?r+=`Assistant: ${a.content}${d}`:a.role==="system"&&(r+=`${a.content}
|
| 22 |
+
|
| 23 |
+
`);return r+="Assistant:",n.encode(r)}function ct(e,n,c){var m;const t=nt();let d=e,r="";((m=d[0])==null?void 0:m.role)==="system"&&(r=d[0].content.trim(),d=d.slice(1));let a=`<|begin_of_text|><|start_header_id|>system<|end_header_id|>
|
| 24 |
+
|
| 25 |
+
`;a+=`Cutting Knowledge Date: December 2023
|
| 26 |
+
Today Date: ${t}
|
| 27 |
+
|
| 28 |
+
`,a+=`${r}<|eot_id|>`;for(const $ of d)a+=`<|start_header_id|>${$.role}<|end_header_id|>
|
| 29 |
+
|
| 30 |
+
${$.content.trim()}<|eot_id|>`;return a+=`<|start_header_id|>assistant<|end_header_id|>
|
| 31 |
+
|
| 32 |
+
`,n.encode(a)}function it(e,n,c){var T;const t=(c==null?void 0:c.variant)??"v1",d=(c==null?void 0:c.bosToken)??"<s>",r=(c==null?void 0:c.eosToken)??"</s>";let a=e,m="";((T=a[0])==null?void 0:T.role)==="system"&&(m=a[0].content,a=a.slice(1));const $=m===""?-1:t==="v1"?a.findIndex(y=>y.role==="user"):a.length-1;let E=d;for(let y=0;y<a.length;y++){const b=a[y];if(b.role==="assistant"){const x=t==="ministral"?b.content.trim():b.content;E+=t==="v1"?` ${x} ${r}`:` ${x}${r}`;continue}const _=y===$?`${m}
|
| 33 |
+
|
| 34 |
+
${b.content}`:b.content;E+=t==="v1"?` [INST] ${_} [/INST]`:t==="nemo"?`[INST] ${_}[/INST]`:`[INST]${_}[/INST]`}return n.encode(E)}function rt(e,n,c){const t=(c==null?void 0:c.eosToken)??"<|endoftext|>";let d=(c==null?void 0:c.bosToken)??"";for(let r=0;r<e.length;r++){const a=e[r],m=r===e.length-1;d+=a.role==="assistant"?`<|assistant|>
|
| 35 |
+
${a.content}${t}${m?"":`
|
| 36 |
+
`}`:`<|${a.role}|>
|
| 37 |
+
${a.content}
|
| 38 |
+
`,m&&(d+=`<|assistant|>
|
| 39 |
+
`)}return n.encode(d)}function lt(e){const n=new URLSearchParams(e);return D(B(n.get("model")),Number(n.get("ctx")))}async function dt(e,n){const c=await F(e);return e.tokenizerKind==="byteLevel"?X(n,c):K(n,c)}function ft(e,n,c){const t=e.chatTemplateId;return t==="chatml"?tt(n,c):t==="deepseek"?ot(n,c):t==="llama3"?ct(n,c):t==="mistral"||t==="mistral-nemo"||t==="ministral"?it(n,c,{variant:t==="mistral"?"v1":t==="mistral-nemo"?"nemo":"ministral"}):t==="olmo2"||t==="falcon3"?rt(n,c,t==="olmo2"?{bosToken:"<|endoftext|>"}:void 0):q(n,c)}export{pt as SHIPPED_MODELS,ft as buildChatPromptFor,dt as loadTokenizerFor,mt as modelBranding,lt as specFromSearch};
|
assets/preload-helper-ckwbz45pO3.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
const v="modulepreload",E=function(i){return"/"+i},d={},y=function(f,l,p){let c=Promise.resolve();if(l&&l.length>0){let n=function(t){return Promise.all(t.map(o=>Promise.resolve(o).then(s=>({status:"fulfilled",value:s}),s=>({status:"rejected",reason:s}))))};document.getElementsByTagName("link");const e=document.querySelector("meta[property=csp-nonce]"),u=(e==null?void 0:e.nonce)||(e==null?void 0:e.getAttribute("nonce"));c=n(l.map(t=>{if(t=E(t),t in d)return;d[t]=!0;const o=t.endsWith(".css"),s=o?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${t}"]${s}`))return;const r=document.createElement("link");if(r.rel=o?"stylesheet":v,o||(r.as="script"),r.crossOrigin="",r.href=t,u&&r.setAttribute("nonce",u),document.head.appendChild(r),o)return new Promise((m,h)=>{r.addEventListener("load",m),r.addEventListener("error",()=>h(new Error(`Unable to preload CSS for ${t}`)))})}))}function a(n){const e=new Event("vite:preloadError",{cancelable:!0});if(e.payload=n,window.dispatchEvent(e),!e.defaultPrevented)throw n}return c.then(n=>{for(const e of n||[])e.status==="rejected"&&a(e.reason);return f().catch(a)})};export{y as _};
|
assets/room-host-C6R3zseuU5.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{q as pe}from"./chat-ui-CQAxV1po6S.js";import{o as ue}from"./model-registry-BoK-ObgACo.js";const Y=240*1024,Z=8*1024*1024,ge=8*1024*1024,ye=1*1024*1024;async function H(e,s){var i;if(!((i=navigator.storage)!=null&&i.getDirectory))return null;try{return await(await navigator.storage.getDirectory()).getDirectoryHandle(ue(e),{create:s})}catch{return null}}async function R(e){const s=[];for await(const[i,c]of e){if(c.kind!=="file")continue;const h=await c.getFile();s.push({name:i,size:h.size})}return s.sort((i,c)=>i.size-c.size),s}const he=e=>[...new Uint8Array(e)].map(s=>s.toString(16).padStart(2,"0")).join("");async function B(e){const s=e.slice();return he(await crypto.subtle.digest("SHA-256",s))}function ee(e,s){const i=new TextEncoder().encode(JSON.stringify(e)),c=new Uint8Array(4+i.byteLength+s.byteLength);return new DataView(c.buffer).setUint32(0,i.byteLength,!0),c.set(i,4),c.set(s,4+i.byteLength),c.buffer}function me(e){const s=new DataView(e).getUint32(0,!0);return{header:JSON.parse(new TextDecoder().decode(new Uint8Array(e,4,s))),payload:new Uint8Array(e,4+s)}}function we(e,s,i){e.bufferedAmountLowThreshold=ye,e.binaryType="arraybuffer";async function c(){for(;e.readyState==="open"&&e.bufferedAmount>ge;)await new Promise(d=>{const a=()=>{clearTimeout(n),e.removeEventListener("bufferedamountlow",a),d()},n=setTimeout(a,50);e.addEventListener("bufferedamountlow",a,{once:!0})})}e.addEventListener("message",d=>{if(typeof d.data!="string")return;let a;try{a=JSON.parse(d.data)}catch{return}a.type==="inventory"?h():a.type==="pull"&&S(a.have??[])});async function h(){const d=await H(s,!1),a=d?await R(d):[],n={files:a.length,bytes:a.reduce((u,g)=>u+g.size,0),names:a.map(u=>u.name)};e.send(JSON.stringify({type:"inventory",...n})),i==null||i(`inventory: ${n.files} files, ${(n.bytes/1e9).toFixed(2)} GB`)}async function S(d){const a=await H(s,!1);if(!a){e.send(JSON.stringify({type:"pull-error",message:"no cached weights on the host"}));return}const n=new Set(d),u=(await R(a)).filter(g=>!n.has(g.name));i==null||i(`serving ${u.length} files (${(u.reduce((g,y)=>g+y.size,0)/1e9).toFixed(2)} GB)`);try{for(const{name:g,size:y}of u){const E=await(await a.getFileHandle(g)).getFile();if(y===0){e.send(ee({name:g,off:0,total:0,sha:await B(new Uint8Array(0))},new Uint8Array(0)));continue}for(let m=0;m<y;m+=Z){const I=new Uint8Array(await E.slice(m,Math.min(m+Z,y)).arrayBuffer());for(let O=0;O<I.byteLength;O+=Y){const N=I.subarray(O,Math.min(O+Y,I.byteLength)),v={name:g,off:m+O,total:y,sha:await B(N)};if(await c(),e.readyState!=="open")return;e.send(ee(v,N))}}}e.send(JSON.stringify({type:"pull-done"})),i==null||i("serve complete")}catch(g){e.send(JSON.stringify({type:"pull-error",message:g instanceof Error?g.message:String(g)}))}}}function Oe(e,s=15e3){return new Promise((i,c)=>{const h=setTimeout(()=>{d(),c(new Error("peer did not answer the inventory request"))},s),S=a=>{if(typeof a.data!="string")return;const n=JSON.parse(a.data);n.type==="inventory"&&(d(),i({files:n.files,bytes:n.bytes,names:n.names}))},d=()=>{clearTimeout(h),e.removeEventListener("message",S)};e.addEventListener("message",S),e.send(JSON.stringify({type:"inventory"}))})}async function be(e){const s=await H(e,!1);return s?(await R(s)).map(i=>i.name):[]}async function Ae(e,s,i,c){e.binaryType="arraybuffer";const h=await H(s,!0);if(!h)throw new Error("this browser has no OPFS β cannot cache weights here");const S=new Set(await be(s)),d=(c==null?void 0:c.bytes)??0,a=(c==null?void 0:c.files)??0,n=performance.now();let u=S.size,g=0,y=null;return await new Promise((M,E)=>{const m=v=>{e.removeEventListener("message",O),v?E(v):M({files:u,bytes:g,seconds:(performance.now()-n)/1e3})};let I=Promise.resolve();const O=v=>{I=I.then(()=>N(v))};async function N(v){if(typeof v.data=="string"){const p=JSON.parse(v.data);p.type==="pull-done"?m():p.type==="pull-error"&&m(new Error(p.message??"peer failed to serve"));return}try{const{header:p,payload:w}=me(v.data);if(await B(w)!==p.sha)throw new Error(`${p.name}+${p.off}: piece failed its SHA-256 β transfer corrupted`);if(!y||y.name!==p.name){if(y)throw new Error(`peer moved to ${p.name} with ${y.name} incomplete`);y={name:p.name,buf:new Uint8Array(p.total),got:0}}if(y.buf.set(w,p.off),y.got+=w.byteLength,g+=w.byteLength,y.got>=p.total){const C=await(await h.getFileHandle(y.name,{create:!0})).createWritable();await C.write(y.buf),await C.close(),y=null,u++}i==null||i({filesDone:u,filesTotal:a,bytesDone:g,bytesTotal:d,rate:g/Math.max((performance.now()-n)/1e3,.001)})}catch(p){m(p instanceof Error?p:new Error(String(p)))}}e.addEventListener("message",O),e.send(JSON.stringify({type:"pull",have:[...S]}))})}const j=8;function te(e,s,i){const c=new Uint8Array(j+i.byteLength),h=new DataView(c.buffer);return h.setUint32(0,e,!0),h.setUint32(4,s,!0),c.set(new Uint8Array(i),j),c.buffer}function ne(e){const s=new DataView(e);return{reqId:s.getUint32(0,!0),position:s.getUint32(4,!0),residual:e.slice(j)}}function Ee(e,s,i){e.binaryType="arraybuffer";let c=Promise.resolve(),h=0;e.addEventListener("message",S=>{typeof S.data!="string"&&(c=c.then(async()=>{const{reqId:d,position:a,residual:n}=ne(S.data);try{const u=await s.pipelineStep({residual:n},a);"tokenId"in u?e.send(JSON.stringify({type:"stage-token",req:d,tokenId:u.tokenId})):e.send(te(d,a,u.residual)),++h%32===1&&(i==null||i(`served ${h} token${h===1?"":"s"}`))}catch(u){e.send(JSON.stringify({type:"stage-error",req:d,message:u instanceof Error?u.message:String(u)}))}}))})}function $e(e){e.binaryType="arraybuffer";const s=new Map;let i=1,c=0,h=0;const S=(d,a)=>{const n=s.get(d);n&&(s.delete(d),a(n))};return e.addEventListener("message",d=>{if(typeof d.data!="string"){const{reqId:n,residual:u}=ne(d.data);S(n,g=>{c++,h+=performance.now()-g.t0,g.resolve({residual:u})});return}const a=JSON.parse(d.data);S(a.req,n=>{a.type==="stage-token"&&typeof a.tokenId=="number"?(c++,h+=performance.now()-n.t0,n.resolve({tokenId:a.tokenId})):n.reject(new Error(a.message??"the other stage failed"))})}),e.addEventListener("close",()=>{for(const d of s.values())d.reject(new Error("a stage of the model disconnected"));s.clear()}),{step(d,a){if(e.readyState!=="open")return Promise.reject(new Error("a stage of the model is not connected"));const n=i++,u=new Promise((g,y)=>{s.set(n,{resolve:g,reject:y,t0:performance.now()})});return e.send(te(n,d,a)),u},meanHopMs:()=>c?h/c:0}}const Se="wss://zero-tvm-share-signal.abgunaydin94.workers.dev";function ve(){return{base:Se,override:null}}const ke={iceServers:[{urls:["stun:stun.cloudflare.com:3478","stun:stun.l.google.com:19302"]}]};function Ne(e){const{spec:s,brand:i,param:c,engine:h,tokenizer:S,encode:d,stageRange:a,ui:n}=e,{base:u}=ve(),g=crypto.getRandomValues(new Uint8Array(16)),y=e.existingRoom??btoa(String.fromCharCode(...g)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),M=`${location.origin}${e.guestPath??"/share.html"}#${y}`,E=new WebSocket(`${u}/room/${y}?role=host`),m=new Map,I=new Map,O=[];let N=!1;const v=new Map,p=(o,t)=>{var l;const r=(l=m.get(o))==null?void 0:l.dc;(r==null?void 0:r.readyState)==="open"&&r.send(JSON.stringify(t))},w=[],U=new Map,C=()=>w.length?w[w.length-1].end:a.end,q=()=>!!a&&C()===s.layers;async function se(o,t,r,l){if(!q())throw new Error(`layers ${C()}-${s.layers} of this model are not connected`);const f=[...w],k=async(L,T)=>{let D=await h.pipelineStep({tokenId:L},T);for(const A of f){if("tokenId"in D)throw new Error("a stage ended the model before the last one");D=await A.step(T,D.residual)}if(!("tokenId"in D))throw new Error("the last stage returned a residual, not a token");return D.tokenId};let b=0;for(let L=0;L<o.length;L++)b=await k(o[L],L);const $=[];for(let L=0;L<t&&!(s.stops.includes(b)||l());L++)$.push(b),r(b),b=await k(b,o.length+L);return $}async function G(){var k,b,$,L,T,D;if(N)return;const o=O.shift();if(!o)return;N=!0;const{guest:t,req:r}=o,l=((k=r.messages.at(-1))==null?void 0:k.content.slice(0,80))??"",f=n.row(t,l);(b=e.lock)!=null&&b.held()&&f("waiting β the host is chattingβ¦"),await(($=e.lock)==null?void 0:$.acquire());try{const A=d(r.messages),P=s.maxContext-A.length;if(P<16)throw new Error(`prompt is ${A.length} tokens β over this model's ${s.maxContext}-token context`);const F=[],le=performance.now();f("generatingβ¦"),(L=n.onBusy)==null||L.call(n,!0);const K=z=>{var x;F.push(z),(x=n.onToken)==null||x.call(n),p(t,{type:"text",id:r.id,full:S.decode(F)})},Q=()=>v.get(t)===r.id||!m.has(t);a?await se(A,P,K,Q):await h.generatePipelined(A,P,K,Q);const de=(performance.now()-le)/1e3,X=F.length/Math.max(de,.001);p(t,{type:"done",id:r.id,tokens:F.length,tps:X});const fe=w.reduce((z,x)=>z+x.meanHopMs(),0);f(`${F.length} tok Β· ${X.toFixed(1)} tok/s`+(w.length?` Β· ${fe.toFixed(1)} ms/hop across ${w.length} stage${w.length===1?"":"s"}`:""))}catch(A){p(t,{type:"error",id:r.id,message:A instanceof Error?A.message:String(A)}),f("error")}finally{(T=e.lock)==null||T.release(),N=!1,(D=n.onBusy)==null||D.call(n,!1),G()}}function re(o,t){let r;try{r=JSON.parse(t)}catch{return}if(r.type==="stop"){v.set(o,r.id);return}if(r.type==="stage-offer"){oe(o,r);return}if(r.type!=="chat"||!Array.isArray(r.messages))return;v.delete(o),O.push({guest:o,req:r});const l=O.length-(N?0:1);l>0&&p(o,{type:"busy",id:r.id,pos:l}),G()}let V={hosts:1,guests:0};function J(){var o,t;(o=n.onMembers)==null||o.call(n,{...V,connected:m.size}),(t=n.onChain)==null||t.call(n,a?ae():"")}function ae(){const o=`${a.start}-${a.end} here`,t=w.map(l=>`${l.start}-${l.end}`).join(" β "),r=q()?"":` Β· waiting for layers ${C()}-${s.layers}`;return`split model β layers ${o}${t?` β ${t}`:""}${r}`}function oe(o,t){const r=I.get(o),l=a?t.specId!==s.id?`different model (${t.specId} vs ${s.id})`:r?t.start<t.end?t.end>s.layers?`layers ${t.start}-${t.end} runs past this model's ${s.layers}`:t.start<a.end?`layers ${t.start}-${t.end} overlaps the ${a.start}-${a.end} this host holds`:w.some(f=>t.start<f.end&&f.start<t.end)?`layers ${t.start}-${t.end} overlap a stage already in the chain`:null:`layers ${t.start}-${t.end} is not a range`:"the pipeline channel is not open":"this host runs the whole model";if(l){p(o,{type:"stage-reject",message:l}),n.row(o,`stage offer refused οΏ½οΏ½ ${l}`);return}if(U.set(o,t),W(),U.has(o)){const f=`layers ${t.start}-${t.end} held β the chain needs ${C()} next`;p(o,{type:"stage-wait",message:f}),n.row(o,f)}}function W(){var o;for(;;){const t=C(),r=[...U].find(([,$])=>$.start===t);if(!r)break;const[l,f]=r,k=I.get(l);if(!k){U.delete(l);continue}U.delete(l);const b=$e(k);w.push({guest:l,start:f.start,end:f.end,step:b.step,meanHopMs:b.meanHopMs}),p(l,{type:"stage-accept",start:f.start,end:f.end}),n.row(l,`serving layers ${f.start}-${f.end}`)}J(),q()&&(n.row("room",`the model is complete across ${w.length+1} stages`),(o=n.onPaired)==null||o.call(n,!0))}function ie(o){var l;const t=w.findIndex(f=>f.guest===o);if(t<0){U.delete(o);return}const r=w.splice(t);for(const f of r.slice(1))m.has(f.guest)&&U.set(f.guest,{type:"stage-offer",start:f.start,end:f.end,specId:s.id});W(),(l=n.onPaired)==null||l.call(n,q())}function ce(o){var b;(b=m.get(o))==null||b.pc.close();const t=new RTCPeerConnection(ke),r={pc:t,dc:null};m.set(o,r);const l=t.createDataChannel("chat",{ordered:!0});r.dc=l;const f=t.createDataChannel("weights",{ordered:!0});we(f,s,$=>n.row(o,`weights β ${$}`));const k=t.createDataChannel("pipeline",{ordered:!0});k.binaryType="arraybuffer",I.set(o,k),l.onopen=()=>{p(o,{type:"info",name:i.name,params:i.params,rateLabel:i.rateLabel,tag:pe(s),param:c,specId:s.id}),J()},l.onmessage=$=>re(o,String($.data)),t.onicecandidate=$=>{$.candidate&&E.send(JSON.stringify({to:o,type:"ice",candidate:$.candidate}))},t.createOffer().then(async $=>{await t.setLocalDescription($),E.send(JSON.stringify({to:o,type:"offer",sdp:$}))}),J()}E.onmessage=o=>{var l,f,k;const t=JSON.parse(String(o.data));if(t.type==="room"){V={hosts:t.hosts??1,guests:t.guests??0},J();return}const r=t.from;r&&(t.type==="peer-joined"?ce(r):t.type==="peer-left"?((l=m.get(r))==null||l.pc.close(),m.delete(r),I.delete(r),a&&(w.some(b=>b.guest===r)||U.has(r))&&(n.row(r,"a stage of the model left"),ie(r)),J()):t.type==="answer"&&t.sdp?(f=m.get(r))==null||f.pc.setRemoteDescription(t.sdp):t.type==="ice"&&t.candidate&&((k=m.get(r))==null||k.pc.addIceCandidate(t.candidate)))};const _=()=>E.close();return window.addEventListener("beforeunload",_),J(),{roomId:y,link:M,close(){window.removeEventListener("beforeunload",_),E.close();for(const[,o]of m)o.pc.close();m.clear(),I.clear()}}}export{ke as I,ve as a,Oe as f,Ne as h,Ae as p,Ee as s};
|
assets/share-TXmFQ6ZAWJ.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js","assets/model-select-DhhV9mCi0T.js","assets/loading-ui-CjCwSRuXvh.js","assets/engine-core-CLR3cxsqf4.js","assets/variants-De-VVc9BRz.js"])))=>i.map(i=>d[i]);
|
| 2 |
+
import{a as J}from"./model-registry-BoK-ObgACo.js";import{_ as x}from"./preload-helper-ckwbz45pO3.js";import{w as U,a as G,b as W,c as z,s as K}from"./chat-ui-CQAxV1po6S.js";import{h as j,I as R,s as Y,a as Z,f as Q,p as X}from"./room-host-C6R3zseuU5.js";const{base:B}=Z(),n=h=>document.getElementById(h);function ee(h){const t=h.replace(/^#/,"");return/^[A-Za-z0-9_-]{16,64}$/.test(t)?t:null}function te(h){const t=/^(\d+)-(\d+)$/.exec(new URLSearchParams(h).get("layers")??"");return t?{start:Number(t[1]),end:Number(t[2])}:null}const T=ee(location.hash),ne=new URLSearchParams(location.search).has("model"),I=te(location.search);T&&I&&I.start>0?se(T,I):T&&!ne?ae(T):oe(T,I);function H(){const h=n("awake");if(!h)return;let t=null,r=null;const b=async c=>{if(c){try{t=await navigator.wakeLock.request("screen")}catch{}if(!r){r=new AudioContext;const i=r.createOscillator(),f=r.createGain();f.gain.value=1e-4,i.connect(f).connect(r.destination),i.start()}r.resume()}else t==null||t.release().catch(()=>{}),t=null,r==null||r.suspend()};h.addEventListener("change",()=>void b(h.checked)),document.addEventListener("visibilitychange",()=>{h.checked&&document.visibilityState==="visible"&&b(!0)})}async function V(h,t){const{isModelCached:r}=await x(async()=>{const{isModelCached:i}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:i}},__vite__mapDeps([0,1,2])),b=await r(h),c=document.createElement("dialog");c.id="share-gate",c.innerHTML=`
|
| 3 |
+
<h2>Host ${t.name} from this tab</h2>
|
| 4 |
+
<p>
|
| 5 |
+
Hosting runs the model on THIS machine and serves it to whoever opens
|
| 6 |
+
your room link. ${b?"The weights are already cached on this device.":`The weights download once (${t.sizeLabel}) and are cached locally; every later visit starts from disk.`}
|
| 7 |
+
</p>
|
| 8 |
+
${t.ramNote?`<p class="warn">${t.ramNote}</p>`:""}
|
| 9 |
+
<p class="fine">Guests' prompts run on your GPU. You see every request as it arrives.</p>
|
| 10 |
+
<div class="acts">
|
| 11 |
+
<a href="/">Not now</a>
|
| 12 |
+
<button type="button" id="share-gate-go" autofocus>${b?"Start hosting β":"Download & host β"}</button>
|
| 13 |
+
</div>`,document.body.appendChild(c),c.showModal(),await new Promise(i=>{var f;(f=c.querySelector("#share-gate-go"))==null||f.addEventListener("click",()=>{c.close(),c.remove(),i()},{once:!0})})}async function oe(h,t){if(n("host-view").classList.remove("hidden"),!("gpu"in navigator)){const o=n("loading-error-top");o.style.display="block",o.textContent="Hosting needs WebGPU with shader-f16 β Chrome and Edge ship it; Safari from 26. This browser can still JOIN a room as a guest: guests run nothing locally.",n("badge").className="badge error",n("badge-text").textContent="No WebGPU";return}const[{specFromSearch:r,modelBranding:b,buildChatPromptFor:c},i,f,_]=await Promise.all([x(()=>import("./model-select-DhhV9mCi0T.js"),__vite__mapDeps([3,2,1])),x(()=>import("./loading-ui-CjCwSRuXvh.js"),__vite__mapDeps([4,1,2,3,5,6])),x(()=>import("./variants-De-VVc9BRz.js"),__vite__mapDeps([6,2])),x(()=>import("./engine-core-CLR3cxsqf4.js"),__vite__mapDeps([5,2,6]))]),g=r(location.search),m=b(g);if(n("page-title").textContent=t?`Sharing ${m.name} β layers ${t.start}-${t.end} here`:`Sharing ${m.name}`,t&&t.start!==0)throw new Error(`share: a hosting stage must start at layer 0 (got ${t.start}); later stages join a room as helpers`);H(),await V(g,m);const l=(()=>{if(!g.moe)return 0;const o=new URLSearchParams(location.search).get("pool");if(!o)return 0;const v=g.moe.experts,e=o==="half"?Math.round(v/2):o==="quarter"?Math.round(v/4):Number(o);return Number.isFinite(e)&&e>0?e:0})(),w=await i.bootEngine({spec:g,optionalFeatures:["subgroups"],probeSubgroups:!0,layerRange:t??void 0,...l?{expertPool:l}:{},buildEngine:({device:o,weights:v,sgSizeOk:e,spec:s})=>{const u=f.parseVariantFlags(location.search,{hasSubgroupsFeature:o.features.has("subgroups"),sgSizeOk:e}),k=!s.qkNorm&&s.weightFormat!=="mlx-safetensors";window.__variants={...u,fused:k};const P=_.allocKVPages(o,s);return _.buildDecodeEngine(o,v,P,{variants:u,fused:k,spec:s,layerRange:t??void 0,...l?{expertPool:l}:{}})},warmup:t?async o=>{await o.pipelineStep({tokenId:1},0)}:void 0});if(!w.ok){const o=n("loading-error-top");o.style.display="block",o.textContent=w.reason,n("loading-error").textContent=w.reason;return}const{engine:p,tokenizer:$}=w,E=(o,v)=>{var s;(s=n("req-empty"))==null||s.remove();const e=document.createElement("li");return e.innerHTML='<span class="who"></span> Β· <span class="body"></span> <span class="st"></span>',e.querySelector(".who").textContent=o,e.querySelector(".body").textContent=v,n("req-log").prepend(e),e.querySelector(".st")};let a="1 machine serving Β· 0 guests connected",d="";function y(){n("room-stats").textContent=d?`${d} Β· ${a}`:a}const S=j({spec:g,brand:l?{...m,rateLabel:""}:m,param:new URLSearchParams(location.search).get("model")??"",engine:p,tokenizer:$,encode:o=>c(g,o,$),existingRoom:h,stageRange:t,guestPath:location.pathname,ui:{row:(o,v)=>{const e=E(o,v);return s=>{e.textContent=s}},onMembers:({hosts:o,guests:v})=>{a=`${o} ${o===1?"machine":"machines"} serving Β· ${v} ${v===1?"guest":"guests"} connected`,y()},onChain:o=>{d=o,y()},onPaired:o=>{window.__stagePaired=o}}});h&&(n("page-title").textContent=`Serving ${m.name} in a shared room`),n("share-link").value=S.link,n("copy-link").addEventListener("click",()=>{navigator.clipboard.writeText(S.link),n("copy-link").textContent="Copied",setTimeout(()=>{n("copy-link").textContent="Copy"},1200)}),window.__shareLink=S.link,window.__shareReady=!0}async function se(h,t){var E;n("host-view").classList.remove("hidden"),(E=n("share-link").closest("section"))==null||E.remove(),H();const[{specFromSearch:r,modelBranding:b},c,i,f]=await Promise.all([x(()=>import("./model-select-DhhV9mCi0T.js"),__vite__mapDeps([3,2,1])),x(()=>import("./loading-ui-CjCwSRuXvh.js"),__vite__mapDeps([4,1,2,3,5,6])),x(()=>import("./variants-De-VVc9BRz.js"),__vite__mapDeps([6,2])),x(()=>import("./engine-core-CLR3cxsqf4.js"),__vite__mapDeps([5,2,6]))]),_=r(location.search),g=b(_);n("page-title").textContent=`Serving ${g.name} layers ${t.start}-${t.end}`;const m=n("room-stats");m.textContent=`loading layers ${t.start}-${t.end} of ${_.layers}β¦`,await V(_,g);const l=await c.bootEngine({spec:_,optionalFeatures:["subgroups"],probeSubgroups:!0,layerRange:t,buildEngine:({device:a,weights:d,sgSizeOk:y,spec:S})=>{const o=i.parseVariantFlags(location.search,{hasSubgroupsFeature:a.features.has("subgroups"),sgSizeOk:y});return window.__variants={...o,fused:!1},f.buildDecodeEngine(a,d,f.allocKVPages(a,S),{variants:o,fused:!1,spec:S,layerRange:t})},warmup:async a=>{await a.pipelineStep({residual:new ArrayBuffer(_.d*2)},0)}});if(!l.ok){const a=n("loading-error-top");a.style.display="block",a.textContent=l.reason,n("loading-error").textContent=l.reason;return}const w=new WebSocket(`${B}/room/${h}?role=guest`),p=new RTCPeerConnection(R);let $=null;p.onicecandidate=a=>{a.candidate&&w.send(JSON.stringify({type:"ice",candidate:a.candidate}))},p.ondatachannel=a=>{if(a.channel.label==="pipeline"){Y(a.channel,l.engine,d=>{m.textContent=`layers ${t.start}-${t.end} Β· ${d}`});return}a.channel.label==="chat"&&($=a.channel,$.onopen=()=>$.send(JSON.stringify({type:"stage-offer",start:t.start,end:t.end,specId:_.id})),$.onmessage=d=>{const y=JSON.parse(String(d.data));y.type==="stage-accept"?(m.textContent=`in the chain β this device runs layers ${t.start}-${t.end} of ${_.layers}`,window.__helperPaired=!0):y.type==="stage-wait"?m.textContent=`holding layers ${t.start}-${t.end} β ${y.message}`:y.type==="stage-reject"&&(m.textContent=`the host refused this stage: ${y.message}`)})},w.onmessage=async a=>{const d=JSON.parse(String(a.data));if(d.type==="offer"&&d.sdp){await p.setRemoteDescription(d.sdp);const y=await p.createAnswer();await p.setLocalDescription(y),w.send(JSON.stringify({type:"answer",sdp:y}))}else d.type==="ice"&&d.candidate?await p.addIceCandidate(d.candidate):(d.type==="no-host"||d.type==="host-left")&&(m.textContent="no host in this room to pair with β open the room link on the machine holding the first layers")},window.__helperReady=!0}async function ae(h){n("guest-view").classList.remove("hidden");const t=e=>{n("guest-status").textContent=e},r=(e,s)=>{n("guest-badge").className=`badge ${s}`,n("guest-badge-text").textContent=e};r("Connecting","loading"),U();const b=new WebSocket(`${B}/room/${h}?role=guest`);let c=new RTCPeerConnection(R),i=null,f=()=>{},_=new Promise(e=>{f=e});const g=[];let m=0,l=null,w="";const p=n("inp"),$=n("btn"),E=n("stop-btn"),a=e=>{$.hidden=e,E.hidden=!e,$.disabled=e},d=()=>{l=null,a(!1)},y=e=>{e.type==="info"?(K(e.name,e.tag),n("guest-model").textContent=`${e.name} β remote`,t(`${e.params}${e.rateLabel?` Β· ${e.rateLabel}`:""} Β· runs on the host machine β the host can read what you send; this page holds only the conversation.`),r("Ready","ready"),p.disabled=!1,p.placeholder="Message the remote modelβ¦",$.disabled=!1,o(e.param,e.specId)):e.type==="text"?(w=e.full,l==null||l.render(w)):e.type==="done"?(l&&(l.finish({fullText:w,tokens:e.tokens,tokPerS:e.tps}),g.push({role:"assistant",content:w})),d()):e.type==="busy"?t(`Host is generating for someone else β queue position ${e.pos}.`):e.type==="error"&&(l&&(l.body.textContent=`β ${e.message}`),d())};function S(){c.onicecandidate=e=>{e.candidate&&b.send(JSON.stringify({type:"ice",candidate:e.candidate}))},c.ondatachannel=e=>{if(e.channel.label==="weights"){const u=e.channel;u.binaryType="arraybuffer",u.readyState==="open"?f(u):u.addEventListener("open",()=>f(u),{once:!0});return}if(e.channel.label!=="chat")return;i=e.channel,i.onmessage=u=>y(JSON.parse(String(u.data)));const s=i;i.onclose=()=>{i===s&&r("Disconnected","error")}}}S(),b.onmessage=async e=>{const s=JSON.parse(String(e.data));if(s.type==="no-host")r("No host","error"),t("Nobody is serving a model in this room β every host tab is closed, or the link expired.");else if(s.type==="host-left")r("Host left","error"),t("Every machine serving this room went away. The conversation stays here; it resumes if one comes back."),p.disabled=!0,$.disabled=!0;else if(s.type==="host-changed")r("Switching host","loading"),t("The machine serving you went away; another one in the room is taking overβ¦"),c.close(),c=new RTCPeerConnection(R),i=null,_=new Promise(u=>{f=u}),S(),d(),p.disabled=!0,$.disabled=!0;else if(s.type==="room"){const u=s.hosts??0;n("room-count").textContent=u>1?`${u} machines serving`:""}else if(s.type==="offer"&&s.sdp){await c.setRemoteDescription(s.sdp);const u=await c.createAnswer();await c.setLocalDescription(u),b.send(JSON.stringify({type:"answer",sdp:u}))}else s.type==="ice"&&s.candidate&&await c.addIceCandidate(s.candidate)};async function o(e,s){const u=n("local-copy"),k=J(e);if(k.id!==s)return;const P=await Promise.race([_,new Promise(F=>setTimeout(()=>F(null),2e4))]);if(!P)return;const D=n("lc-status"),L=n("lc-btn");let N;try{N=await Q(P)}catch{return}if(!N.files)return;const O=(N.bytes/1e9).toFixed(2);u.classList.remove("hidden"),D.textContent=`The host has ${O} GB cached. Copying it here lets this device run ${k.id} on its own GPU.`,L.textContent=`Copy ${O} GB to this device`,L.addEventListener("click",()=>{L.disabled=!0;const F=performance.now();X(P,k,C=>{const M=C.bytesTotal?Math.round(C.bytesDone/C.bytesTotal*100):0;D.textContent=`${M}% Β· ${(C.bytesDone/1e9).toFixed(2)}/${O} GB Β· ${C.filesDone}/${C.filesTotal} files Β· ${(C.rate/1e6).toFixed(0)} MB/s`},N).then(C=>{const M=((performance.now()-F)/1e3).toFixed(0),A=encodeURIComponent(e),q="";D.innerHTML=`Done β ${(C.bytes/1e9).toFixed(2)} GB in ${M}s. <a href="/zero-tvm.html?model=${A}">Open the chat on this device β</a><br><a href="/share.html?model=${A}${q}#${h}">β¦or serve this room from here too β</a>`,L.remove(),window.__pullDone=C}).catch(C=>{D.textContent=`Copy failed: ${C.message}`,L.disabled=!1})})}const v=()=>{const e=p.value.trim();!e||l||!i||i.readyState!=="open"||(p.value="",G(p),g.push({role:"user",content:e}),W(e),l=z(),l.showThinking(),w="",a(!0),i.send(JSON.stringify({type:"chat",id:++m,messages:[...g]})))};n("composer").addEventListener("submit",e=>{e.preventDefault(),v()}),E.addEventListener("click",()=>{(i==null?void 0:i.readyState)==="open"&&i.send(JSON.stringify({type:"stop",id:m}))}),p.addEventListener("input",()=>G(p)),p.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),v())})}
|
assets/validate-D2Aic8RAjT.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{m as w}from"./model-registry-BoK-ObgACo.js";import{specFromSearch as x,buildChatPromptFor as v}from"./model-select-DhhV9mCi0T.js";import{bootEngine as S,hideProgress as M,setBadge as g,log as E}from"./loading-ui-CjCwSRuXvh.js";import"./weight-loader-ByOLMd9f5C.js";import"./engine-core-CLR3cxsqf4.js";import"./variants-De-VVc9BRz.js";const y=x(location.search),h=[{label:"factual-recall",system:"You are a helpful assistant.",user:"What is the capital of France?",hint:"Paris"},{label:"arithmetic",system:"You are a helpful assistant. Answer briefly.",user:"What is 17 plus 25?",hint:"42"},{label:"code-completion",system:"You are a coding assistant.",user:"Write a one-line Python expression that returns the length of a list called xs.",hint:"len"},{label:"instruction-follow",system:"You are a helpful assistant.",user:'Reply with exactly the word "yes" and nothing else.',hint:"yes"},{label:"open-ended",system:"You are a helpful assistant.",user:"Name three colors."}];function p(t){E(t)}function u(t){const s=document.getElementById("status");s&&(s.textContent=t)}function b(t){const s=document.getElementById("results-wrap"),a=document.createElement("div");a.className="prompt-result",a.innerHTML=t,s.appendChild(a)}function m(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function F(t,s,a){let i=-1/0;for(let o=0;o<t.length;o++)t[o]>i&&(i=t[o]);let c=0;const e=new Float32Array(t.length);for(let o=0;o<t.length;o++)e[o]=Math.exp(t[o]-i),c+=e[o];const n=new Set,r=[];for(let o=0;o<s;o++){let l=-1,f=-1/0;for(let d=0;d<t.length;d++)n.has(d)||t[d]>f&&(f=t[d],l=d);if(l<0)break;n.add(l),r.push({id:l,logit:f,prob:e[l]/c,text:a.decode([l])})}return r}function k(t){let s=-1/0;for(let e=0;e<t.length;e++)t[e]>s&&(s=t[e]);let a=0;for(let e=0;e<t.length;e++)a+=Math.exp(t[e]-s);const i=Math.log(a)+s;let c=0;for(let e=0;e<t.length;e++){const n=Math.exp(t[e]-i);n>1e-12&&(c-=n*Math.log(n))}return c}async function P(t,s,a){const i=v(t.spec,[{role:"system",content:a.system},{role:"user",content:a.user}],s);t.resetKVTracking();const c=performance.now(),e=await t.forwardLogits(i),n=performance.now()-c,r=F(e,10,s),o=k(e),l=[],f=performance.now();await t.generate(i,i.length,24,$=>{l.push($)});const d=performance.now()-f;return{label:a.label,prompt:a.user,topK:r,entropyNats:o,continuation:s.decode(l),forwardMs:n,decodeMs:d,decodeTokens:l.length}}function I(t){const s=t.topK.map((a,i)=>{const c=(a.prob*100).toFixed(2),e="β".repeat(Math.max(1,Math.round(a.prob*30))),n=JSON.stringify(a.text).slice(1,-1);return` ${String(i+1).padStart(2)}. ${n.padEnd(20)} ${c.padStart(6)}% ${e}`}).join(`
|
| 2 |
+
`);return`
|
| 3 |
+
<div class="prompt-label">${m(t.label)}</div>
|
| 4 |
+
<div class="prompt-text">${m(t.prompt)}</div>
|
| 5 |
+
<div class="prompt-meta">
|
| 6 |
+
forward: ${t.forwardMs.toFixed(0)} ms Β·
|
| 7 |
+
decode: ${t.decodeMs.toFixed(0)} ms (${t.decodeTokens} tok, ${(t.decodeTokens/(t.decodeMs/1e3)).toFixed(1)} tok/s) Β·
|
| 8 |
+
entropy: ${t.entropyNats.toFixed(2)} nats
|
| 9 |
+
</div>
|
| 10 |
+
<div class="prompt-section">Top-10 next tokens</div>
|
| 11 |
+
<pre class="topk">${m(s)}</pre>
|
| 12 |
+
<div class="prompt-section">Greedy continuation (β€24 tokens)</div>
|
| 13 |
+
<pre class="continuation">${m(t.continuation)}</pre>
|
| 14 |
+
`}async function R(){var c,e;if(!navigator.gpu){u("No WebGPU available");const n=document.getElementById("start-btn");n&&(n.disabled=!0,n.textContent="WebGPU not available in this browser");const r=document.querySelector(".start-screen .desc");r&&(r.textContent="This harness needs WebGPU with shader-f16 β Chrome and Edge ship it; Safari from 26.");return}if(y.id!=="phi3-mini"){const n=w(y),r=document.querySelector(".start-hint");r&&(r.textContent=`${n.sizeLabel} ${n.name} weights download on first run, cached after`)}const t=document.getElementById("start-btn");await new Promise(n=>{t.addEventListener("click",()=>{t.disabled=!0,t.textContent="Loading...",n()})}),(c=document.getElementById("start-screen"))==null||c.remove(),(e=document.getElementById("results-wrap"))==null||e.classList.add("active"),u("Booting engine...");const s=await S({spec:y});if(!s.ok){u(s.reason);return}const{tokenizer:a,engine:i}=s;M(),g("Running","loading"),u(`Running ${h.length} test prompts...`);for(let n=0;n<h.length;n++){const r=h[n];u(`[${n+1}/${h.length}] ${r.label}`),p(`
|
| 15 |
+
--- ${r.label} ---`),p(`prompt: ${r.user}`);try{const o=await P(i,a,r);p(`top-1: ${JSON.stringify(o.topK[0].text)} (${(o.topK[0].prob*100).toFixed(1)}%)`),p(`entropy: ${o.entropyNats.toFixed(2)} nats`),p(`continuation: ${JSON.stringify(o.continuation)}`),b(I(o))}catch(o){p(`ERROR: ${o}`),b(`<div class="prompt-error">${m(r.label)}: ${m(String(o))}</div>`)}}u("Done"),g("Ready","ready"),p(`
|
| 16 |
+
=== validation complete ===`)}R().catch(t=>{console.error(t),p(`FATAL: ${t}`),u(`FATAL: ${t}`)});
|
assets/variants-De-VVc9BRz.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{P as q}from"./model-registry-BoK-ObgACo.js";const w={subgroups:!1,sgAttn:!1,sgArgmax:!1,sgQkv:!1,qkvTile:!1,qkvTile2:!1,sgFfn:!1,matmul:"scalar",int8KV:!1,vec4:!1,vec4Half:!1,vec4Qkv:!1,vec4Affine:!1,vec4Moe:!1,fuseQkNorm:!1,splitK:0,fusePrologue:!1};function G(i,e){const t=new URLSearchParams(i),l=t.get("sg")!=="0";return{subgroups:l&&e.hasSubgroupsFeature&&e.sgSizeOk,sgAttn:l&&t.get("sgattn")!=="0",sgArgmax:l&&t.get("sgargmax")!=="0",sgQkv:l&&t.get("sgqkv")!=="0",qkvTile:l&&t.get("qkvtile")==="1",qkvTile2:l&&t.get("qkvtile2")==="1",sgFfn:l&&t.get("sgffn")!=="0",matmul:t.get("matmul")??(l?"tiled":"scalar"),int8KV:t.get("kv8")!=="0",vec4:l&&t.get("vec4")!=="0",vec4Half:l&&t.get("vec4")!=="0"&&t.get("vec4h")!=="0",vec4Qkv:l&&t.get("vec4qkv")!=="0",vec4Affine:l&&t.get("vec4")!=="0"&&t.get("vec4aff")!=="0",vec4Moe:l&&t.get("vec4moe")==="1",fuseQkNorm:t.get("fuseqk")!=="0",splitK:h(t.get("splitk")??(l?"8":"0")),fusePrologue:t.get("fuseprologue")==="1"}}function h(i){const e=Number.parseInt(i??"0",10);return Number.isFinite(e)&&e>=2&&e<=16?e:0}function m(i,e,t=!1,l,r=t,o=!1,c=!1){if(o){const u=l!==void 0&&c&&l%512===0?"vec4h":"none";return u==="vec4h"&&i!=="scalar"&&e.int4MatmulTiledVec4hAffine&&e.int4MatmulF32TiledVec4hAffine?{pipeline:e.int4MatmulTiledVec4hAffine,pipelineF32:e.int4MatmulF32TiledVec4hAffine,rowsPerWG:4,label:"tiled_vec4h_affine"}:u==="vec4h"&&e.int4MatmulSgVec4hAffine&&e.int4MatmulF32SgVec4hAffine?{pipeline:e.int4MatmulSgVec4hAffine,pipelineF32:e.int4MatmulF32SgVec4hAffine,rowsPerWG:1,label:"sg_vec4h_affine"}:i!=="scalar"&&e.int4MatmulTiledAffine&&e.int4MatmulF32TiledAffine?{pipeline:e.int4MatmulTiledAffine,pipelineF32:e.int4MatmulF32TiledAffine,rowsPerWG:4,label:"tiled_affine"}:e.int4MatmulSgAffine&&e.int4MatmulF32SgAffine?{pipeline:e.int4MatmulSgAffine,pipelineF32:e.int4MatmulF32SgAffine,rowsPerWG:1,label:"sg_affine"}:{pipeline:e.int4MatmulAffine,pipelineF32:e.int4MatmulF32Affine,rowsPerWG:1,label:"scalar_affine"}}let s=!1;return l!==void 0&&l%1024!==0&&(s=r&&l%512===0,t=!1),s&&i==="tiled"&&e.int4MatmulTiledVec4h&&e.int4MatmulF32TiledVec4h?{pipeline:e.int4MatmulTiledVec4h,pipelineF32:e.int4MatmulF32TiledVec4h,rowsPerWG:4,label:"tiled_vec4h"}:s&&i==="sg"&&e.int4MatmulSgVec4h&&e.int4MatmulF32SgVec4h?{pipeline:e.int4MatmulSgVec4h,pipelineF32:e.int4MatmulF32SgVec4h,rowsPerWG:1,label:"sg_vec4h"}:t&&i==="tiled"&&e.int4MatmulTiledVec4&&e.int4MatmulF32TiledVec4?{pipeline:e.int4MatmulTiledVec4,pipelineF32:e.int4MatmulF32TiledVec4,rowsPerWG:4,label:"tiled_vec4"}:t&&i==="sg"&&e.int4MatmulSgVec4&&e.int4MatmulF32SgVec4?{pipeline:e.int4MatmulSgVec4,pipelineF32:e.int4MatmulF32SgVec4,rowsPerWG:1,label:"sg_vec4"}:i==="tiled8"&&e.int4MatmulTiled8&&e.int4MatmulF32Tiled8?{pipeline:e.int4MatmulTiled8,pipelineF32:e.int4MatmulF32Tiled8,rowsPerWG:8,label:"tiled8"}:i==="tiled"&&e.int4MatmulTiled&&e.int4MatmulF32Tiled?{pipeline:e.int4MatmulTiled,pipelineF32:e.int4MatmulF32Tiled,rowsPerWG:4,label:"tiled"}:i==="sg"&&e.int4MatmulSg&&e.int4MatmulF32Sg?{pipeline:e.int4MatmulSg,pipelineF32:e.int4MatmulF32Sg,rowsPerWG:1,label:"sg"}:{pipeline:e.int4Matmul,pipelineF32:e.lmHead,rowsPerWG:1,label:"scalar"}}function W(i,e,t=q){const l=t.weightFormat==="mlx-safetensors",{pipeline:r,pipelineF32:o,rowsPerWG:c,label:s}=m(i.matmul,e,i.vec4,t.d,i.vec4Half,l,i.vec4Affine),{pipeline:u}=m(i.matmul,e,i.vec4,t.qDim,i.vec4Half,l,i.vec4Affine),{pipeline:v}=m(i.matmul,e,i.vec4,t.ffn,i.vec4Half,l,i.vec4Affine),d=i.sgAttn&&e.attentionSg?e.attentionSg:e.attention,f=i.splitK>=2?i.splitK:0,g=f?i.sgAttn&&e.attentionSplitKSg?e.attentionSplitKSg:e.attentionSplitK:null,S=f?e.attentionCombine:null,M=g?`splitk${f}${g===e.attentionSplitKSg?"_sg":""}`:d===e.attentionSg?"_sg":"scalar",p=i.sgArgmax&&e.argmaxSg?e.argmaxSg:e.argmax,n=i.vec4Qkv&&t.d%1024===0&&e.qkvFusedSgVec4?e.qkvFusedSgVec4:i.qkvTile2&&e.qkvFusedTiled2Sg?e.qkvFusedTiled2Sg:i.qkvTile&&e.qkvFusedTiledSg?e.qkvFusedTiledSg:i.sgQkv&&e.qkvFusedSg?e.qkvFusedSg:e.qkvFused,T=n===e.qkvFusedTiledSg||n===e.qkvFusedTiled2Sg?2:1,k=n===e.qkvFusedSgVec4?"_sg_vec4":n===e.qkvFusedTiled2Sg?"tiled2_sg":n===e.qkvFusedTiledSg?"tiled_sg":n===e.qkvFusedSg?"_sg":"scalar",F=i.fusePrologue,a=F?i.sgFfn&&e.fusedFfnTiledSgPrologue?e.fusedFfnTiledSgPrologue:e.fusedFfnPrologue:i.sgFfn&&e.fusedFfnTiledSg?e.fusedFfnTiledSg:e.fusedFfn,A=a===e.fusedFfnTiledSg||a===e.fusedFfnTiledSgPrologue?4:1,V=a===e.fusedFfnTiledSgPrologue?"tiled_sg_prologue":a===e.fusedFfnPrologue?"scalar_prologue":a===e.fusedFfnTiledSg?"tiled_sg":"scalar";return{matmul:r,matmulF32:o,matmulOProj:u,matmulFfnDown:v,matmulRowsPerWG:c,matmulLabel:s,attention:d,attentionLabel:M,attentionSplitK:g,attentionCombine:S,splitK:g?f:0,argmax:p,argmaxLabel:p===e.argmaxSg?"_sg":"scalar",qkvFused:n,qkvPairsPerWG:T,qkvLabel:k,ffn:a,ffnRowsPerWG:A,ffnLabel:V,ffnPrologue:F}}export{w as SCALAR_VARIANTS,G as parseVariantFlags,m as resolveMatmul,W as resolveVariantPipelines};
|
assets/weight-loader-ByOLMd9f5C.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{P as ie,o as ye,W as _e}from"./model-registry-BoK-ObgACo.js";function ge(t){if(t.byteLength<8)throw new Error("safetensors: need at least 8 bytes for the header length");const e=new DataView(t),o=e.getUint32(0,!0);if(e.getUint32(4,!0)!==0)throw new Error("safetensors: header longer than 4 GB");if(t.byteLength<8+o)throw new Error(`safetensors: header is ${o} bytes, only ${t.byteLength-8} supplied`);const l=JSON.parse(new TextDecoder().decode(new Uint8Array(t,8,o))),a={};for(const[n,s]of Object.entries(l)){if(n==="__metadata__")continue;const i=s;a[n]={dtype:i.dtype,shape:i.shape,begin:i.data_offsets[0],end:i.data_offsets[1]}}return{tensors:a,dataStart:8+o}}function Be(t){const e=new Uint16Array(t.length);let o=0;for(let r=0;r<t.length;r++){const l=t[r],a=l&32768,n=l>>7&255,s=(l&127)<<16;if(n===255){e[r]=a|31744|(s?512:0);continue}const i=n-112;if(i>=31){e[r]=a|31743,o++;continue}if(i<=0){if(i<-10){e[r]=a;continue}const f=s|8388608,c=14-i;let u=f>>>c;const w=f&(1<<c)-1,d=1<<c-1;(w>d||w===d&&u&1)&&u++,e[r]=a|u;continue}e[r]=a|i<<10|s>>>13}return{out:e,overflow:o}}function ve(t){const e=new Float32Array(t.length),o=new Uint32Array(e.buffer);for(let r=0;r<t.length;r++)o[r]=t[r]<<16;return e}const K=new Float32Array(1),ce=new Uint32Array(K.buffer);function re(t){K[0]=t;const e=ce[0],o=e>>>16&32768;let r=e>>>23&255;const l=e&8388607;if(r===255)return o|31744|(l?512:0);if(r=r-127+15,r>=31)return o|31744;if(r<=0){if(r<-10)return o;const s=l|8388608,i=14-r;let f=s>>>i;const c=s&(1<<i)-1,u=1<<i-1;return(c>u||c===u&&f&1)&&f++,o|f}let a=r<<10|l>>>13;const n=l&8191;return(n>4096||n===4096&&a&1)&&a++,o|a}function ne(t){const e=(t&32768)<<16,o=t>>>10&31,r=t&1023;let l;if(o===0)if(r===0)l=e;else{let a=-1,n=r;do a++,n<<=1;while(!(n&1024));l=e|112-a<<23|(n&1023)<<13}else o===31?l=e|2139095040|r<<13:l=e|o-15+127<<23|r<<13;return ce[0]=l>>>0,K[0]}function fe(t,e,o){const r=`${`${t.mlxPrefix??""}model.`}layers.${e}`,l=s=>[{record:`${s}.weight`,convert:"raw"},{record:`${s}.scales`,convert:"bf16->f16"},{record:`${s}.biases`,convert:"bf16->f16"}],a=(s,i)=>{const f=i.map(l);return[{name:`${s}_w`,parts:f.map(c=>c[0])},{name:`${s}_s`,parts:f.map(c=>c[1])},{name:`${s}_b`,parts:f.map(c=>c[2])}]},n=[{name:"norm1",parts:[{record:`${r}.input_layernorm.weight`,convert:"bf16->f16"}]},{name:"norm2",parts:[{record:`${r}.post_attention_layernorm.weight`,convert:"bf16->f16"}]}];if(t.layerKinds[e]==="attn"&&t.mla){const s=t.mla,i=s.qkRopeHeadDim,f=d=>d<i/2?2*d:2*(d-i/2)+1,c=Array.from({length:t.mlaQProjRows},(d,b)=>b);for(let d=0;d<t.heads;d++){const b=d*(s.qkNopeHeadDim+i)+s.qkNopeHeadDim;for(let $=0;$<i;$++)c[b+$]=b+f($)}const u=Array.from({length:t.mlaKvaRows},(d,b)=>b);for(let d=0;d<i;d++)u[s.kvLoraRank+d]=s.kvLoraRank+f(d);const w=(d,b)=>d.map($=>({...$,op:b}));n.push(...w(a("mla_q",[`${r}.self_attn.q_proj`]),{kind:"permuteRows",rows:t.mlaQProjRows,src:c})),n.push(...w(a("mla_kva",[`${r}.self_attn.kv_a_proj_with_mqa`]),{kind:"permuteRows",rows:t.mlaKvaRows,src:u})),n.push({name:"mla_kva_norm",parts:[{record:`${r}.self_attn.kv_a_layernorm.weight`,convert:"bf16->f16"}]}),n.push({name:"mla_kvb",parts:l(`${r}.self_attn.kv_b_proj`),op:{kind:"dequantAffineSplit",bits:4,group:64,k:s.kvLoraRank,heads:t.heads,nope:s.qkNopeHeadDim,v:s.vHeadDim}}),n.push(...a("o_proj",[`${r}.self_attn.o_proj`]))}else if(t.layerKinds[e]==="attn")n.push(...a("c_attn",[`${r}.self_attn.q_proj`,`${r}.self_attn.k_proj`,`${r}.self_attn.v_proj`])),n.push(...a("o_proj",[`${r}.self_attn.o_proj`])),t.qkNorm&&(n.push({name:"q_norm",parts:[{record:`${r}.self_attn.q_norm.weight`,convert:"bf16->f16"}]}),n.push({name:"k_norm",parts:[{record:`${r}.self_attn.k_norm.weight`,convert:"bf16->f16"}]}));else{const s=`${r}.linear_attn`;n.push(...a("gdn_proj",[`${s}.in_proj_qkv`,`${s}.in_proj_z`,`${s}.in_proj_a`,`${s}.in_proj_b`])),n.push(...a("gdn_out",[`${s}.out_proj`])),n.push({name:"gdn_conv",parts:[{record:`${s}.conv1d.weight`,convert:"bf16->f16"}]}),n.push({name:"gdn_norm",parts:[{record:`${s}.norm.weight`,convert:"bf16->f16"}]}),n.push({name:"gdn_a_log",parts:[{record:`${s}.A_log`,convert:"bf16->f32"}]}),n.push({name:"gdn_dt_bias",parts:[{record:`${s}.dt_bias`,convert:"bf16->f32"}]})}if(t.ffnKinds[e]==="moe"&&t.moe){const s=t.moe.sharedExpert??!0;for(const i of["gate_proj","up_proj","down_proj"])n.push(...a(`moe_${i}`,s?[`${r}.mlp.switch_mlp.${i}`,`${r}.mlp.shared_expert.${i}`]:[`${r}.mlp.switch_mlp.${i}`]));if(t.moe.routerBits===16){const i=s?[`${r}.mlp.gate`,`${r}.mlp.shared_expert_gate`]:[`${r}.mlp.gate`];n.push({name:"router_w",parts:i.map(f=>({record:`${f}.weight`,convert:"bf16->f16"}))})}else n.push(...a("router",s?[`${r}.mlp.gate`,`${r}.mlp.shared_expert_gate`]:[`${r}.mlp.gate`]))}else n.push(...a("ffn",[`${r}.mlp.gate_proj`,`${r}.mlp.up_proj`])),n.push(...a("ffn_down",[`${r}.mlp.down_proj`]));return n}function Se(t,e,o){const r=t.mlxPrefix??"",l=((o==null?void 0:o.start)??0)===0,a=((o==null?void 0:o.end)??t.layers)===t.layers,n=t.paramNaming.biasFor;if(!n)throw new Error(`${t.id}: planGlobal needs paramNaming.biasFor (affine checkpoints only)`);const s=(i,f)=>[{name:`${i}_w`,parts:[{record:`${f}.weight`,convert:"raw"}]},{name:`${i}_s`,parts:[{record:`${f}.scales`,convert:"bf16->f16"}]},{name:`${i}_b`,parts:[{record:n(`${f}.weight`),convert:"bf16->f16"}]}];return[...l||a&&t.tiedEmbeddings?s("embed",`${r}model.embed_tokens`):[],...t.tiedEmbeddings||!a?[]:s("lm_head",`${r}lm_head`),...a?[{name:"final_norm",parts:[{record:`${r}model.norm.weight`,convert:"bf16->f16"}]}]:[]]}function Ee(t,e,o){var n,s;const r=[];let l=0;for(const i of t.parts){const f=e(i.record),c=(o==null?void 0:o(i.record))??"BF16";if(i.convert==="raw")r.push(f);else if(i.convert==="bf16->f16"&&c==="F16")r.push(f);else if(i.convert==="bf16->f32"&&c==="F32")r.push(f);else{if(c!=="BF16")throw new Error(`${t.name}: ${i.record} is ${c}, expected BF16 for ${i.convert}`);{const u=new Uint16Array(f.buffer,f.byteOffset,f.byteLength/2);if(i.convert==="bf16->f16"){const w=Be(u);l+=w.overflow,r.push(new Uint8Array(w.out.buffer))}else r.push(new Uint8Array(ve(u).buffer))}}}let a;if(((n=t.op)==null?void 0:n.kind)==="dequantAffineSplit")a=ke(t.name,t.op,r);else{const i=r.reduce((c,u)=>c+u.byteLength,0);a=new Uint8Array(new ArrayBuffer(i));let f=0;for(const c of r)a.set(c,f),f+=c.byteLength;((s=t.op)==null?void 0:s.kind)==="permuteRows"&&(a=xe(t.name,t.op,a))}if(l>0)throw new Error(`${t.name}: ${l} bf16 value(s) exceed f16 range and were clamped β these weights cannot be represented at f16 and the model would run wrong`);return{data:a,overflow:l}}function xe(t,e,o){if(e.src.length!==e.rows)throw new Error(`${t}: permuteRows has ${e.src.length} source indices for ${e.rows} rows`);if(o.byteLength%e.rows!==0)throw new Error(`${t}: ${o.byteLength} B does not divide into ${e.rows} rows`);const r=o.byteLength/e.rows,l=new Uint8Array(new ArrayBuffer(o.byteLength));for(let a=0;a<e.rows;a++){const n=e.src[a];if(n<0||n>=e.rows)throw new Error(`${t}: permuteRows src[${a}] = ${n} is out of range`);l.set(o.subarray(n*r,(n+1)*r),a*r)}return l}function ke(t,e,o){if(o.length!==3)throw new Error(`${t}: dequantAffineSplit needs exactly [weight, scales, biases], got ${o.length} parts`);const{k:r,heads:l,nope:a,v:n,group:s,bits:i}=e,f=32/i,c=l*(a+n),u=r/f,w=r/s,[d,b,$]=o,p=(m,g,j)=>{if(m!==g)throw new Error(`${t}: ${j} is ${g} B, expected ${m} B for [${c}, ${r}] at ${i} bits`)};p(c*u*4,d.byteLength,"weight"),p(c*w*2,b.byteLength,"scales"),p(c*w*2,$.byteLength,"biases");const _=new DataView(d.buffer,d.byteOffset,d.byteLength),v=new DataView(b.buffer,b.byteOffset,b.byteLength),M=new DataView($.buffer,$.byteOffset,$.byteLength),k=new Uint16Array(c*r),D=l*r*a,F=new Float32Array(r);let B=0;const T=(1<<i)-1;for(let m=0;m<l;m++)for(let g=0;g<a+n;g++){const j=m*(a+n)+g;for(let x=0;x<w;x++){const S=ne(v.getUint16((j*w+x)*2,!0)),O=ne(M.getUint16((j*w+x)*2,!0));for(let H=x*s;H<(x+1)*s;H++){const z=_.getUint32((j*u+(H/f|0))*4,!0);F[H]=(z>>>H%f*i&T)*S+O}}if(g<a){const x=m*r*a+g;for(let S=0;S<r;S++)Math.abs(F[S])>65504&&B++,k[x+S*a]=re(F[S])}else{const x=D+m*n*r+(g-a)*r;for(let S=0;S<r;S++)Math.abs(F[S])>65504&&B++,k[x+S]=re(F[S])}}if(B>0)throw new Error(`${t}: ${B} dequantized value(s) exceed f16 range β these weights cannot be represented at f16 and the model would run wrong`);return new Uint8Array(k.buffer)}async function je(t){const o=JSON.parse(new TextDecoder().decode(await t.whole("model.safetensors.index.json"))).weight_map,r=[...new Set(Object.values(o))].sort(),l={};for(const n of r){const s=await t.range(n,0,8),i=new DataView(s.buffer,s.byteOffset,8);if(i.getUint32(4,!0)!==0)throw new Error(`${n}: safetensors header longer than 4 GB`);const f=i.getUint32(0,!0),c=await t.range(n,0,8+f);l[n]=ge(c.buffer.slice(c.byteOffset,c.byteOffset+c.byteLength))}return{locate:n=>{const s=o[n];if(!s)throw new Error(`record not in checkpoint index: ${n}`);const i=l[s],f=i.tensors[n];if(!f)throw new Error(`record in index but not in ${s}'s header: ${n}`);return{file:s,begin:i.dataStart+f.begin,end:i.dataStart+f.end,info:f}},records:Object.keys(o),shards:r}}function le(t,e){const o=(e==null?void 0:e.start)??0,r=(e==null?void 0:e.end)??t.layers,l=[];for(const a of Se(t,void 0,e))l.push({plan:a,layer:null});for(let a=o;a<r;a++)for(const n of fe(t,a))l.push({plan:n,layer:a});return l}function de(t,e){const o=e===null?`g.${t.name}`:`l${e}.${t.name}`;return t.op?`${o}.${Fe(t)}`:o}function Fe(t){const e=t.op,o=e.kind==="permuteRows"?`rows=${e.rows};src=${e.src.join(",")}`:`bits=${e.bits};group=${e.group};k=${e.k};heads=${e.heads};nope=${e.nope};v=${e.v}`,r=`${e.kind};${o};${t.parts.map(a=>`${a.record}:${a.convert}`).join(",")}`;let l=2166136261;for(let a=0;a<r.length;a++)l^=r.charCodeAt(a),l=Math.imul(l,16777619);return((l>>>16^l)&65535).toString(16).padStart(4,"0")}async function ae(t,e,o){const r=new Map;for(const a of t.parts){const n=e(a.record);r.set(a.record,await o.range(n.file,n.begin,n.end))}const{data:l}=Ee(t,a=>{const n=r.get(a);if(!n)throw new Error(`internal: range for ${a} was not fetched`);return n},a=>e(a).info.dtype);return l}const We={embed_w:["root","embdWeights"],embed_s:["root","embdScales"],embed_b:["root","embdBiases"],lm_head_w:["root","lmHeadWeights"],lm_head_s:["root","lmHeadScales"],lm_head_b:["root","lmHeadBiases"],final_norm:["root","finalNormGamma"],norm1:["layer","normGamma1"],norm2:["layer","normGamma2"],c_attn_w:["layer","qkvWeights"],c_attn_s:["layer","qkvScales"],c_attn_b:["layer","qkvBiases"],o_proj_w:["layer","oProjWeights"],o_proj_s:["layer","oProjScales"],o_proj_b:["layer","oProjBiases"],q_norm:["layer","qNormGamma"],k_norm:["layer","kNormGamma"],mla_q_w:["mla","qWeights"],mla_q_s:["mla","qScales"],mla_q_b:["mla","qBiases"],mla_kva_w:["mla","kvaWeights"],mla_kva_s:["mla","kvaScales"],mla_kva_b:["mla","kvaBiases"],mla_kva_norm:["mla","kvaNormGamma"],mla_kvb:["mla","kvbF16"],gdn_proj_w:["gdn","projWeights"],gdn_proj_s:["gdn","projScales"],gdn_proj_b:["gdn","projBiases"],gdn_out_w:["gdn","outWeights"],gdn_out_s:["gdn","outScales"],gdn_out_b:["gdn","outBiases"],gdn_conv:["gdn","convWeight"],gdn_norm:["gdn","normGamma"],gdn_a_log:["gdn","aLog"],gdn_dt_bias:["gdn","dtBias"],moe_gate_proj_w:["moe","gateWeights"],moe_gate_proj_s:["moe","gateScales"],moe_gate_proj_b:["moe","gateBiases"],moe_up_proj_w:["moe","upWeights"],moe_up_proj_s:["moe","upScales"],moe_up_proj_b:["moe","upBiases"],moe_down_proj_w:["moe","downWeights"],moe_down_proj_s:["moe","downScales"],moe_down_proj_b:["moe","downBiases"],router_w:["moe","routerWeights"],router_s:["moe","routerScales"],router_b:["moe","routerBiases"],ffn_w:["layer","ffnWeights"],ffn_s:["layer","ffnScales"],ffn_b:["layer","ffnBiases"],ffn_down_w:["layer","ffnDownWeights"],ffn_down_s:["layer","ffnDownScales"],ffn_down_b:["layer","ffnDownBiases"]};function Ae(t){const e=/^moe_(gate|up|down)_proj_([wsb])$/.exec(t);return e?[e[1],e[2]]:null}async function Le(t,e,o,r,l,a={},n){var w,d,b,$,p;const s=le(e,n),i={device:t},f=Array.from({length:e.layers},()=>({}));let c=0,u=0;for(const{plan:_,layer:v}of s){const M=We[_.name];if(!M)throw new Error(`assembleMlx: no LoadedWeights slot for plan '${_.name}'`);const k=de(_,v),D=g=>{var S;const[j,x]=M;if(j==="root")i[x]=g;else{if(v===null)throw new Error(`assembleMlx: '${_.name}' is a layer plan with no layer`);if(j==="layer")f[v][x]=g;else{const O=(S=f[v])[j]??(S[j]={});O[x]=g}}},F=v!==null&&a.expertPool?Ae(_.name):null;if(F&&a.expertPool){const{slots:g,source:j}=a.expertPool,x=j.slabBytes(v,F[0],F[1]);if(a.cacheWrite&&!await((w=a.cacheHas)==null?void 0:w.call(a,k))){const O=await ae(_,r,o);a.cacheWrite(k,O.buffer.slice(O.byteOffset,O.byteOffset+O.byteLength))}const S=t.createBuffer({size:g*x,usage:l,label:`${k}.slots`});D(S),u+=g*x,c++,(d=a.onBuffer)==null||d.call(a,c,s.length,u);continue}let B;const T=await((b=a.cacheRead)==null?void 0:b.call(a,k));T?B=new Uint8Array(T):(B=await ae(_,r,o),($=a.cacheWrite)==null||$.call(a,k,B.buffer.slice(B.byteOffset,B.byteOffset+B.byteLength)));const m=t.createBuffer({size:B.byteLength,usage:l,label:k});t.queue.writeBuffer(m,0,B),u+=B.byteLength,c++,(p=a.onBuffer)==null||p.call(a,c,s.length,u),D(m)}return i.layers=f,e.tiedEmbeddings&&(i.lmHeadWeights=i.embdWeights,i.lmHeadScales=i.embdScales,i.lmHeadBiases=i.embdBiases),i.initNormGamma=f[(n==null?void 0:n.start)??0].normGamma1,i}const Me={BOOL:1,U8:1,I8:1,U16:2,I16:2,F16:2,BF16:2,U32:4,I32:4,F32:4,U64:8,I64:8,F64:8};function Oe(t,e,o,r,l){const a=`moe_${r}_proj_${l}`,n=fe(t,o).find($=>$.name===a);if(!n)throw new Error(`slab-source: ${t.id} layer ${o} has no '${a}' buffer β not a MoE layer`);if(n.op)throw new Error(`slab-source: '${a}' carries a ${n.op.kind} op; its bytes are not the stacked tensor`);if(n.parts.length>2)throw new Error(`slab-source: '${a}' has ${n.parts.length} parts, expected [stacked] or [stacked, shared]`);const s=e(n.parts[0].record),i=s.info.shape;if(i.length!==3)throw new Error(`slab-source: ${n.parts[0].record} has shape [${i}], expected [experts, N, K]`);const f=Me[s.info.dtype];if(!f)throw new Error(`slab-source: ${n.parts[0].record} has unsupported dtype ${s.info.dtype}`);const c=i[0],u=i[1]*i[2]*f,w=s.end-s.begin;if(w!==c*u)throw new Error(`slab-source: ${n.parts[0].record} declares ${w} B but [${i}] at ${s.info.dtype} is ${c*u} B`);if(t.moe&&t.moe.experts!==c)throw new Error(`slab-source: ${t.id} declares ${t.moe.experts} experts but ${n.parts[0].record} is stacked ${c} deep`);let d=null;if(n.parts.length===2){d=e(n.parts[1].record);const $=d.info.shape;if($.length!==2||$[0]!==i[1]||$[1]!==i[2]||d.info.dtype!==s.info.dtype)throw new Error(`slab-source: ${n.parts[1].record} is [${$}] ${d.info.dtype}, cannot stack as expert ${c} of [${i}] ${s.info.dtype}`);if(d.end-d.begin!==u)throw new Error(`slab-source: ${n.parts[1].record} declares ${d.end-d.begin} B, expected one slab of ${u} B`);if(n.parts[1].convert!==n.parts[0].convert)throw new Error(`slab-source: '${a}' converts its two parts differently (${n.parts[0].convert} vs ${n.parts[1].convert})`)}const b=n.parts[0].convert;if(b!=="raw"&&b!=="bf16->f16")throw new Error(`slab-source: '${a}' converts ${b}, which changes the slab's byte length`);return{experts:c,stride:u,stacked:s,shared:d,key:de(n,o),builtBytes:(c+(d?1:0))*u,convert:b,dtype:s.info.dtype}}function qe(t,e){const o=new Map;return(r,l,a)=>{const n=`${r}.${l}.${a}`;let s=o.get(n);return s||o.set(n,s=Oe(t,e,r,l,a)),s}}function Ue(t,e){const o=t.experts+(t.shared?1:0);if(!Number.isInteger(e)||e<0||e>=o)throw new Error(`slab-source: expert ${e} is outside 0..${o-1}`+(t.shared?` (${t.experts} routed + shared)`:` (${t.experts} routed, no shared expert)`));return e*t.stride}function oe(t){return t.replace(/[^A-Za-z0-9._-]/g,"_")}function De(t,e,o){const r=qe(t,e),l=new Map,a=new Map;let n=!0;const s=f=>{let c=l.get(f);return c||(c=o.getFileHandle(oe(f)).catch(u=>{throw l.delete(f),new Error(`slab-source: OPFS has no cached buffer '${oe(f)}' (${u})`)}),l.set(f,c)),c},i=async f=>{if(!n)return null;const c=a.get(f);if(c)return c;const u=await s(f);if(!u.createSyncAccessHandle)return n=!1,null;try{const w=await u.createSyncAccessHandle();return a.set(f,w),w}catch{return n=!1,null}};return{slabBytes:(f,c,u)=>r(f,c,u).stride,read:async(f,c,u,w)=>{const d=r(f,c,u),b=Ue(d,w),$=await i(d.key);if($){const v=$.getSize();if(v!==d.builtBytes)throw new Error(`slab-source: ${d.key} is ${v} B, expected ${d.builtBytes} B (${d.experts}${d.shared?"+1":""} slabs of ${d.stride} B)`);const M=new Uint8Array(new ArrayBuffer(d.stride)),k=$.read(M,{at:b});if(k!==d.stride)throw new Error(`slab-source: ${d.key} read ${k} of ${d.stride} B at ${b}`);return M}const p=await(await s(d.key)).getFile();if(p.size!==d.builtBytes)throw new Error(`slab-source: ${d.key} is ${p.size} B, expected ${d.builtBytes} B (${d.experts}${d.shared?"+1":""} slabs of ${d.stride} B)`);const _=await p.slice(b,b+d.stride).arrayBuffer();if(_.byteLength!==d.stride)throw new Error(`slab-source: ${d.key} returned ${_.byteLength} of ${d.stride} B at ${b}`);return new Uint8Array(_)},close:()=>{for(const f of a.values())try{f.close()}catch{}a.clear(),l.clear()}}}function Q(t,e=500){return e*Math.pow(2,t)*(.5+Math.random())}async function He(t,e,o,r={}){const l=r.retries??0,a=r.baseDelayMs??500,n=typeof e=="function"?e:()=>e,s=new Array(t.length),i=new AbortController;let f,c=!1,u=0;const w=async b=>{for(;!c&&u<t.length&&(b===0||b<n());){const $=u++;for(let p=0;;p++)try{s[$]=await o(t[$],$,i.signal);break}catch(_){if(i.signal.aborted)return;if(p<l){if(await new Promise(v=>setTimeout(v,Q(p,a))),i.signal.aborted)return;continue}c=!0,f=_,i.abort(_ instanceof Error?_:new Error(String(_)));return}}},d=Math.max(1,Math.min(n(),t.length));if(await Promise.all(Array.from({length:d},(b,$)=>w($))),c)throw f instanceof Error?f:new Error(String(f));return s}function Y(t){return`https://huggingface.co/${t.hfRepo}/resolve/main/`}const Ye=Y(ie),ue=8,he=3,C=4;let I=!1;async function Ne(t){var e,o,r,l,a;try{if(typeof navigator>"u"||!((e=navigator.storage)!=null&&e.getDirectory))return{dir:null,persisted:!1};let n=!1;try{n=await((r=(o=navigator.storage).persisted)==null?void 0:r.call(o))??!1,n||(n=await((a=(l=navigator.storage).persist)==null?void 0:a.call(l))??!1)}catch{}const s=await navigator.storage.getDirectory();try{const f=[];for await(const[u,w]of s)w.kind==="directory"&&u.startsWith(`${_e}-`)&&f.push(u);const c=s;await Promise.all(f.map(u=>c.removeEntry(u,{recursive:!0})))}catch{}return{dir:await s.getDirectoryHandle(t,{create:!0}),persisted:n}}catch{return{dir:null,persisted:!1}}}function Z(t){return t.replace(/[^A-Za-z0-9._-]/g,"_")}async function me(t,e){if(!t)return null;try{return await(await(await t.getFileHandle(Z(e))).getFile()).arrayBuffer()}catch{return null}}async function Te(t,e){if(!t)return!1;try{return await t.getFileHandle(Z(e)),!0}catch{return!1}}async function we(t,e,o){if(t)try{const l=await(await t.getFileHandle(Z(e),{create:!0})).createWritable();await l.write(o),await l.close()}catch{}}async function pe(t,e,o,r=C){const l=t.split("/").at(-1);let a=[],n=0,s;const i=f=>{s=f,I||(I=!0,o==null||o(`network unstable β dropping shard concurrency ${ue} β ${he}`))};for(let f=0;f<=r;f++){if(f>0&&(o==null||o(`[retry ${f}/${r}] ${l}`+(n>0?` β resuming at ${(n/1e6).toFixed(1)} MB`:"")+` (${s})`),await new Promise(p=>setTimeout(p,Q(f-1)))),e!=null&&e.aborted)throw e.reason??new DOMException("Aborted","AbortError");let c;try{const p={};n>0&&(p.Range=`bytes=${n}-`),c=await fetch(t,{credentials:"omit",signal:e,headers:p})}catch(p){if(e!=null&&e.aborted)throw p;n>0&&(a=[],n=0),i(p);continue}if(c.status===416){a=[],n=0,s=new Error(`HTTP 416 ${c.statusText}`);continue}if(!c.ok){const p=new Error(`HTTP ${c.status} ${c.statusText}`);if(c.status<500&&c.status!==429&&c.status!==408)throw p;s=p;continue}c.status!==206&&n>0&&(a=[],n=0);const u=c.headers.get("content-length"),w=u?n+Number(u):null;try{if(c.body){const p=c.body.getReader();for(;;){const{done:_,value:v}=await p.read();if(_)break;a.push(v),n+=v.byteLength}}else{const p=await c.arrayBuffer();a.push(new Uint8Array(p)),n+=p.byteLength}}catch(p){if(e!=null&&e.aborted)throw p;i(p);continue}if(w!==null&&n!==w){i(new Error(`truncated body: got ${n} of ${w} bytes`));continue}const d=a.length===1?a[0]:null;if(d&&d.byteOffset===0&&d.byteLength===d.buffer.byteLength)return d.buffer;const b=new Uint8Array(n);let $=0;for(const p of a)b.set(p,$),$+=p.byteLength;return b.buffer}throw s instanceof Error?s:new Error(String(s))}function Re(t){return null}async function Ze(t,e="tokenizer.json"){return Y(t)}async function se(t,e,o,r,l,a,n,s){const i=t.split("/").at(-1),f=w=>{a.push(we(r,e,w))},c=await me(r,e);if(c)return n==null||n(`[opfs] ${i}`),c;for(const w of l)try{const d=await w.match(t);if(d){n==null||n(`[cache] ${i}`);const b=await d.arrayBuffer();return f(b),b}}catch{}n==null||n(`[fetch] ${i}`);const u=await pe(t,s,n);return f(u),u}function Ge(t,e,o,r){const l=a=>t+a;return{whole:async a=>new Uint8Array(await pe(l(a),r,o)),range:async(a,n,s)=>{let i;for(let f=0;f<=C;f++){f>0&&await new Promise(c=>setTimeout(c,Q(f-1)));try{const c=await fetch(l(a),{credentials:"omit",signal:r,headers:{Range:`bytes=${n}-${s-1}`}});if(c.status===200)throw new Error(`${a}: server ignored Range (200, not 206) β cannot stream a multi-GB shard`);if(!c.ok)throw new Error(`${a}: HTTP ${c.status}`);const u=new Uint8Array(await c.arrayBuffer());if(u.byteLength!==s-n)throw new Error(`${a}: asked for ${s-n} bytes, got ${u.byteLength}`);return u}catch(c){i=c,o==null||o(`[retry ${f+1}/${C}] ${a} ${n}-${s} (${c})`)}}throw new Error(`range fetch failed after ${C} retries: ${i}`)}}}async function Ce(){try{if(typeof caches>"u")return[];const t=await caches.keys();return await Promise.all(t.map(e=>caches.open(e)))}catch{return[]}}function ze(t){const e=[];for(const o of t.records)if("records"in o&&Array.isArray(o.records))for(const r of o.records)e.push({...r,dataPath:r.dataPath??o.dataPath});else e.push(o);return e}const V=()=>GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST;function Ie(t,e){return t?e?"OPFS persistent":"OPFS best-effort":"OPFS unavailable"}function Ve(t){if(!isFinite(t)||t<=0)return"β";if(t<60)return`${Math.round(t)}s`;const e=Math.floor(t/60),o=Math.round(t-e*60);return`${e}m ${o}s`}function Ke(t,e,o){const r=o.dtype==="float32"&&o.format==="f32-to-bf16",l=r?o.nbytes*2:o.nbytes;if(l>t.limits.maxStorageBufferBindingSize)throw new Error(`Weight record '${o.name}' (${l} bytes) exceeds device.limits.maxStorageBufferBindingSize (${t.limits.maxStorageBufferBindingSize}). Request a higher requiredLimits.maxStorageBufferBindingSize at device creation.`);const a=t.createBuffer({size:Math.max(l,4),usage:V(),label:o.name});if(r){const n=new Uint16Array(e,o.byteOffset,o.nbytes/2),s=new Uint32Array(n.length);for(let i=0;i<n.length;i++)s[i]=n[i]<<16;return t.queue.writeBuffer(a,0,s),a}return t.queue.writeBuffer(a,0,new Uint8Array(e,o.byteOffset,o.nbytes)),a}async function Je(t,e,o,r=ie,l,a){if(l&&r.weightFormat!=="mlx-safetensors")throw new Error(`loadWeights: layerRange needs an MLX checkpoint; ${r.id} ships MLC shards`);if(a&&r.weightFormat!=="mlx-safetensors")throw new Error(`loadWeights: expertPool needs an MLX checkpoint; ${r.id} ships MLC shards`);const n=Y(r),s=ye(r),i=Re(),f=r.manifestName??"ndarray-cache.json";e==null||e(`Loading ${f}β¦`);const{dir:c,persisted:u}=await Ne(s);e==null||e(c?`OPFS ready (${s}, ${u?"persistent":"best-effort"})`:"OPFS unavailable");const w=await Ce();w.length>0&&(e==null||e(`Cache API: ${w.length} store(s) open`));const d=[];if(r.weightFormat==="mlx-safetensors"){const y=Ge(n,i,e);e==null||e("Reading safetensors headersβ¦");const{locate:h,shards:q}=await je(y),R=le(r,l).length;e==null||e(`${q.length} shards, ${R} buffers to build`);let A;if(a){if(!r.moe)throw new Error(`loadWeights: expertPool is a sparse-MoE feature and ${r.id} has no experts`);if(!c)throw new Error("loadWeights: expertPool has nowhere to read an expert slab from β no dev mirror, and OPFS is unavailable in this runtime");A={source:De(r,h,c),slots:a},e==null||e(`expert pool: ${a} slots per layer, slabs from OPFS`)}const N=performance.now(),W=await Le(t,r,y,h,V(),{onProgress:e,cacheRead:E=>me(c,E),cacheHas:E=>Te(c,E),...A?{expertPool:{slots:A.slots,source:A.source}}:{},cacheWrite:(E,U)=>{d.push(we(c,E,U))},onBuffer:(E,U,G)=>{if(E%25!==0&&E!==U)return;const L=(performance.now()-N)/1e3,$e=G/(E/U);e==null||e(`${E}/${U} buffers β ${(G/1e9).toFixed(2)} GB in ${L.toFixed(0)}s`),o==null||o({shardsLoaded:E,totalShards:U,bytesLoaded:G,totalBytes:Math.round($e),mbPerSec:L>0?G/1e6/L:0,etaSec:E>0?L/E*(U-E):0,persisted:u})}},l);return await Promise.all(d),e==null||e(`Weights ready (${((performance.now()-N)/1e3).toFixed(0)}s)`),A&&(W.moeSlabs=A),W}const b=await se(n+f,f,i,c,w,d,e),$=JSON.parse(new TextDecoder().decode(b)),p=ze($);e==null||e(`Manifest: ${p.length} parameters`);const _=new Map;for(const y of p){const h=_.get(y.dataPath);h?h.push(y):_.set(y.dataPath,[y])}const v=new Map,M=_.size,k=p.reduce((y,h)=>y+h.nbytes,0),D=performance.now();let F=0,B=0;const T=[..._.entries()];try{await He(T,()=>I?he:ue,async([y,h],q,R)=>{let A;try{A=await se(n+y,y,i,c,w,d,e,R)}catch(L){throw R.aborted?L:new Error(`Shard '${y}' failed: ${L instanceof Error?L.message:L}`)}for(const L of h)v.set(L.name,Ke(t,A,L));F++,B+=A.byteLength;const N=(performance.now()-D)/1e3,W=B/1e6/Math.max(N,.1),E=W>0?(k-B)/(W*1e6):0,U=(B/1e6).toFixed(0),G=(k/1e6).toFixed(0);e==null||e(`[${F}/${M}] ${y} Β· ${U}/${G} MB Β· ${W.toFixed(1)} MB/s Β· ETA ${Ve(E)}`),o==null||o({shardsLoaded:F,totalShards:M,bytesLoaded:B,totalBytes:k,mbPerSec:W,etaSec:E,persisted:u})},{retries:1})}catch(y){await Promise.allSettled(d);const h=y instanceof Error?y.message:String(y);throw new Error(`${h} β ${F}/${M} shards (${(B/1e6).toFixed(0)} MB) are safely cached (OPFS); reloading resumes from cache and only re-downloads what's missing. If you use an ad/privacy blocker, allow huggingface.co and hf.co β HF's bot protection can kill downloads when blocked.`)}function m(...y){for(const h of y){const q=v.get(h);if(q)return q}throw new Error(`Weight not found. Tried: ${y.join(", ")}
|
| 2 |
+
Available: ${[...v.keys()].slice(0,20).join(", ")}`)}const g=r.paramNaming,j=m(...g.embedWeight),x=m(...g.embedScale),S=m(...g.layer(0).norm1),O=m(...g.lmHeadWeight),H=m(...g.lmHeadScale),z=m(...g.finalNorm),J=t.createCommandEncoder({label:"gdnProjPack"}),X=[];function P(y,h,q){const R=y.reduce((W,E)=>W+E.rows,0),A=t.createBuffer({size:R*h,usage:V(),label:q});let N=0;for(const W of y)J.copyBufferToBuffer(W.buf,0,A,N,W.rows*h),N+=W.rows*h,X.push(W.buf);return A}const ee=[];for(let y=0;y<r.layers;y++){const h=g.layer(y),q=r.layerKinds[y];ee.push({normGamma1:m(...h.norm1),normGamma2:m(...h.norm2),ffnWeights:m(...h.ffnWeight),ffnScales:m(...h.ffnScale),ffnDownWeights:m(...h.ffnDownWeight),ffnDownScales:m(...h.ffnDownScale),...q==="attn"?{qkvWeights:m(...h.qkvWeight),qkvScales:m(...h.qkvScale),oProjWeights:m(...h.oProjWeight),oProjScales:m(...h.oProjScale),qNormGamma:h.qNorm?m(...h.qNorm):void 0,kNormGamma:h.kNorm?m(...h.kNorm):void 0}:{gdn:{projWeights:P([{buf:m(...h.gdnQkvWeight),rows:r.gdnQkvDim},{buf:m(...h.gdnZWeight),rows:r.gdnVDim},{buf:m(...h.gdnAWeight),rows:r.gdnVHeads},{buf:m(...h.gdnBWeight),rows:r.gdnVHeads}],r.dPacked*4,`gdnProjWeights_${y}`),projScales:P([{buf:m(...h.gdnQkvScale),rows:r.gdnQkvDim},{buf:m(...h.gdnZScale),rows:r.gdnVDim},{buf:m(...h.gdnAScale),rows:r.gdnVHeads},{buf:m(...h.gdnBScale),rows:r.gdnVHeads}],r.dScales*2,`gdnProjScales_${y}`),aLog:m(...h.gdnALog),dtBias:m(...h.gdnDtBias),convWeight:m(...h.gdnConvWeight),normGamma:m(...h.gdnNormWeight),outWeights:m(...h.gdnOutWeight),outScales:m(...h.gdnOutScale)}}})}t.queue.submit([J.finish()]);for(const y of X)y.destroy();d.length>0&&await Promise.allSettled(d);const te=(performance.now()-D)/1e3,be=B/1e6/Math.max(te,.1);return e==null||e(`All weights loaded Β· ${(k/1e6).toFixed(0)} MB in ${te.toFixed(1)}s Β· avg ${be.toFixed(1)} MB/s Β· ${Ie(c,u)}`),{device:t,embdWeights:j,embdScales:x,lmHeadWeights:O,lmHeadScales:H,initNormGamma:S,finalNormGamma:z,layers:ee}}export{Ye as P,de as a,Ve as f,Je as l,Z as o,le as p,Ze as r};
|
assets/zero-tvm-nxGrkTTcQf.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js"])))=>i.map(i=>d[i]);
|
| 2 |
+
import{S as T,m as y}from"./model-registry-BoK-ObgACo.js";import{_ as x}from"./preload-helper-ckwbz45pO3.js";import{specFromSearch as B}from"./model-select-DhhV9mCi0T.js";import{m as q,a as b}from"./mascot-BCKWddkWrV.js";import{setBadge as w}from"./loading-ui-CjCwSRuXvh.js";import{b as N,s as L,w as _}from"./chat-flow-eTAzKAlIYe.js";import{q as $,s as A}from"./chat-ui-CQAxV1po6S.js";import"./weight-loader-ByOLMd9f5C.js";import"./engine-core-CLR3cxsqf4.js";import"./variants-De-VVc9BRz.js";const E=T.filter(e=>!y(e.spec).pending&&e.spec.embeddingOnly!==!0);function k(e){const o=new URL(window.location.href);e?o.searchParams.set("model",e):o.searchParams.delete("model"),window.location.href=o.toString()}function z(e){const o=document.getElementById("model-switch"),t=document.getElementById("model-menu");if(!o||!t)return;const n=new Map;function a(){t.innerHTML="";for(const{param:i,spec:c}of E){const s=y(c),m=c.id===e.id,p=document.createElement("button");p.type="button",p.role="option",p.setAttribute("aria-selected",String(m));const I=n.get(c.id)?'<span class="mm-meta mm-cached">cached</span>':`<span class="mm-meta">${s.sizeLabel} download${s.ramNote?` Β· ${s.ramNote}`:""}</span>`;p.innerHTML=`<span class="mm-name">${s.name}</span>${I}`,m||p.addEventListener("click",()=>k(i)),t.appendChild(p)}}function d(){t.hidden=!0,o.setAttribute("aria-expanded","false")}o.addEventListener("click",i=>{i.stopPropagation();const c=t.hidden;t.hidden=!c,o.setAttribute("aria-expanded",String(c))}),document.addEventListener("click",d),document.addEventListener("keydown",i=>{i.key==="Escape"&&d()}),t.addEventListener("click",i=>i.stopPropagation()),a(),S(e,n),x(()=>import("./cache-probe-BYX_n9OPRB.js"),__vite__mapDeps([0,1,2])).then(async({isModelCached:i})=>{await Promise.all(E.map(async({spec:c})=>n.set(c.id,await i(c)))),a(),S(e,n)}).catch(()=>{})}function S(e,o){const t=document.getElementById("dialog-models");if(t){t.innerHTML="";for(const{param:n,spec:a}of E){const d=y(a),i=a.id===e.id,c=document.createElement("button");c.type="button",c.className="dm-chip",c.setAttribute("aria-pressed",String(i));const s=o.get(a.id)?"cached":d.sizeLabel;c.innerHTML=`<span class="n">${d.name}</span><span class="s">${s}</span>`,i||c.addEventListener("click",()=>k(n)),t.appendChild(c)}}}const u=e=>document.getElementById(e);function U(){const e=new URLSearchParams(location.search),o=(t,n)=>{const a=Number(e.get(t));return Number.isFinite(a)&&e.get(t)!==null?a:n};return{temperature:o("temp",0),topP:o("topp",1),minP:o("minp",0),seed:o("seed",0)}}function W(e){const o=new URL(location.href),t=(n,a,d)=>{a===d?o.searchParams.delete(n):o.searchParams.set(n,String(a))};t("temp",e.temperature,0),t("topp",e.topP??1,1),t("minp",e.minP??0,0),t("seed",e.seed??0,0),history.replaceState(null,"",o.toString())}const v=[{key:"temperature",id:"samp-temp",label:"Temperature",min:0,max:2,step:.05,dflt:0,note:"0 = greedy (argmax). Higher is more varied."},{key:"topP",id:"samp-topp",label:"Top-p",min:.05,max:1,step:.01,dflt:1,note:"Keep the smallest set of tokens covering this much probability. 1 = off."},{key:"minP",id:"samp-minp",label:"Min-p",min:0,max:.5,step:.005,dflt:0,note:"Drop tokens below this fraction of the top token. 0 = off."}];function R(e){const o=u("sampling-btn"),t=u("sampling-menu");if(!o||!t)return;let n=U();const a=v.map(s=>`
|
| 3 |
+
<label class="samp-row" for="${s.id}">
|
| 4 |
+
<span class="samp-label">${s.label}<b id="${s.id}-val"></b></span>
|
| 5 |
+
<input id="${s.id}" type="range" min="${s.min}" max="${s.max}" step="${s.step}">
|
| 6 |
+
<span class="samp-note">${s.note}</span>
|
| 7 |
+
</label>`).join("");t.innerHTML=`
|
| 8 |
+
<div class="samp-head"><b>Sampling</b><span id="samp-mode"></span></div>
|
| 9 |
+
${a}
|
| 10 |
+
<label class="samp-row" for="samp-seed">
|
| 11 |
+
<span class="samp-label">Seed<b id="samp-seed-val"></b></span>
|
| 12 |
+
<div class="samp-seed-row">
|
| 13 |
+
<input id="samp-seed" type="number" min="0" step="1">
|
| 14 |
+
<button type="button" id="samp-reseed" class="samp-btn">New</button>
|
| 15 |
+
</div>
|
| 16 |
+
<span class="samp-note">The same seed replays the same text, token for token.</span>
|
| 17 |
+
</label>
|
| 18 |
+
<button type="button" id="samp-reset" class="samp-btn samp-reset">Back to greedy</button>`;const d=()=>{for(const m of v){const p=n[m.key]??m.dflt;u(m.id).value=String(p),u(`${m.id}-val`).textContent=m.key==="temperature"&&p===0?"off":String(p)}u("samp-seed").value=String(n.seed??0);const s=n.temperature<=0;u("samp-mode").textContent=s?"greedy Β· argmax":"sampling",t.classList.toggle("samp-greedy",s),o.classList.toggle("active",!s),o.title=s?"Sampling: off (greedy)":`Sampling: temp ${n.temperature}`},i=()=>{var s;W(n),d(),(s=e())==null||s.setSampling(n.temperature>0?n:null)};for(const s of v)u(s.id).addEventListener("input",m=>{n={...n,[s.key]:Number(m.target.value)},i()});u("samp-seed").addEventListener("input",s=>{n={...n,seed:Math.max(0,Math.floor(Number(s.target.value)||0))},i()}),u("samp-reseed").addEventListener("click",()=>{n={...n,seed:Math.floor(Math.random()*2**31)},i()}),u("samp-reset").addEventListener("click",()=>{n={temperature:0,topP:1,minP:0,seed:n.seed??0},i()});const c=()=>{t.hidden=!0,o.setAttribute("aria-expanded","false")};o.addEventListener("click",s=>{s.stopPropagation(),t.hidden=!t.hidden,o.setAttribute("aria-expanded",String(!t.hidden))}),t.addEventListener("click",s=>s.stopPropagation()),document.addEventListener("click",c),document.addEventListener("keydown",s=>{s.key==="Escape"&&c()}),d()}const r=B(location.search),l=y(r);function H(){if(!r.moe)return 0;const e=new URLSearchParams(location.search).get("pool");if(!e)return 0;const o=r.moe.experts,t=e==="half"?Math.round(o/2):e==="quarter"?Math.round(o/4):Number(e);return Number.isFinite(t)&&t>0?t:0}const h=H();function O(){if(!h)return;const e=(l.poolModes??[]).find(n=>n.slots===h),o=document.querySelector("#start-dialog .dialog-notes ul");if(!o)return;const t=document.createElement("li");t.textContent=`Memory build: ${e?e.label:`${h} expert slots`}`+(e!=null&&e.note?` β ${e.note}`:""),t.style.color="var(--warn, #d9a441)",o.prepend(t)}const P=$(r);A(l.name,P);const D=e=>document.getElementById(e);function f(e){const o=document.getElementById("progress-log");if(!o){console.log(e);return}o.textContent+=e+`
|
| 19 |
+
`,o.scrollTop=o.scrollHeight}function F(){var e;(e=D("loading-overlay"))==null||e.classList.remove("active")}async function G(){if("serviceWorker"in navigator)try{await navigator.serviceWorker.register("/weights-cache-sw.js",{scope:"/"}),await navigator.serviceWorker.ready}catch(e){console.warn("[zero-tvm] weight-cache SW registration failed:",e)}}function V(){return new Promise(e=>{const o=document.getElementById("start-dialog");if(o!=null&&o.showModal){w("Waiting");const i=o.querySelector("#start-btn");i==null||i.addEventListener("click",()=>{o.close(),o.remove(),e()},{once:!0}),o.showModal();return}const t=document.getElementById("chat");if(!t){e();return}t.innerHTML=`
|
| 20 |
+
<div id="start-screen" class="start-screen" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;gap:1.25rem;text-align:center;padding:2rem">
|
| 21 |
+
<div class="title" style="font-size:1.1rem;font-weight:600;color:#ccc"></div>
|
| 22 |
+
<div class="desc" style="font-size:0.85rem;color:#888;max-width:440px;line-height:1.6">
|
| 23 |
+
First load downloads the ${P} weights and caches them
|
| 24 |
+
locally (OPFS). Every subsequent visit is instant; weights are
|
| 25 |
+
served from the on-device cache.
|
| 26 |
+
</div>
|
| 27 |
+
<button id="start-btn" class="start-btn" style="background:#10b981;color:#000;font-weight:600;font-size:0.95rem;padding:0.7rem 2rem;border:none;border-radius:8px;cursor:pointer">Download & Start</button>
|
| 28 |
+
<div class="start-hint" style="font-size:0.68rem;color:#444"></div>
|
| 29 |
+
</div>`;const n=t.querySelector("#start-screen .title");n&&(n.textContent=`${l.name}, in your browser`);const a=t.querySelector("#start-screen .start-hint");a&&(a.textContent=`${r.hfRepo.split("/")[1]} Β· ${l.sizeLabel} Β· cached after first load`),w("Waiting");const d=document.getElementById("start-btn");d==null||d.addEventListener("click",()=>{d.disabled=!0,d.textContent="Starting...",t.innerHTML="",e()},{once:!0})})}async function M(){w("Initializingβ¦","loading"),f(`Zero-TVM ${l.name} β No WebLLM, No TVM`),f("10 hand-written WGSL kernel roles"),f("");const e=await N({spec:r,poolSlots:h,search:location.search,onDeviceLost:n=>{L(`GPU device lost: ${n.message||n.reason}. Reload the page to recover.`)}});if(!e.ok){L(e.reason,()=>{M()}),f(`ERROR: ${e.reason}`);return}const{tokenizer:o,engine:t}=e;C=t,F(),_({spec:r,tokenizer:o,engine:t,onToken:()=>g==null?void 0:g.pulse(),onPhase:n=>g==null?void 0:g.setMood(n==="generating"?"talking":n==="thinking"?"thinking":"idle"),commands:{roster:()=>{location.href="/"}},log:f})}function j(){const e=(t,n)=>{const a=document.querySelector(t);a&&(a.textContent=n)};e("#welcome-quant",`${$(r)} Β· ${l.params}`),e("#welcome-ctx",`${Math.round(r.maxContext/1024)} K context`);{const t=l.params.split(/[\sΒ·]/)[0],n=document.querySelector('.suggest[data-prompt*="fall short"]');n&&(n.dataset.prompt=`List four kinds of questions where a ${t} model like ${l.name} is likely to fall short compared to a frontier LLM, with one-sentence reasons.`,e('.suggest[data-prompt*="fall short"] .suggest-hint',`Where a ${t} model falls short`))}K();const o=document.querySelectorAll("#start-dialog .dialog-stats .dstat .val");if(o[0]&&(o[0].textContent=l.sizeLabel),o[1]&&(l.rateLabel?o[1].textContent=l.rateLabel:o[1].parentElement.style.display="none"),e("#gate-ctx",`${Math.round(r.maxContext/1024)}K`),l.ramNote){const t=document.querySelector("#start-dialog .dialog-notes ul");if(t){const n=document.createElement("li");n.textContent=l.ramNote,n.style.color="var(--warn, #d9a441)",t.prepend(n)}}O(),r.id!=="phi3-mini"&&(document.title=`Zero-TVM Chat β ${l.name}`,e("#start-dialog .dialog-title",`${l.name} on raw WebGPU`),e(".model-info h1",l.name),e("#welcome .welcome-title",`Chat with ${l.name}`))}let g=null;function K(){var t;const e=document.querySelector(".model-info");if(e&&!document.getElementById("header-mascot")){const n=document.createElement("canvas");n.id="header-mascot",n.className="header-mascot",n.setAttribute("aria-hidden","true"),(t=e.parentElement)==null||t.insertBefore(n,e),b(n,r).then(a=>{if(!a){n.remove();return}g=a,h&&r.moe&&a.setSpec(r,!1,h/(r.moe.experts+(r.sharedExpertIndex>=0?1:0)))}).catch(()=>n.remove())}const o=document.getElementById("welcome-logo");if(o&&!o.querySelector("canvas")){const n=document.createElement("canvas");n.setAttribute("aria-hidden","true"),o.appendChild(n),o.classList.add("has-mascot"),b(n,r).then(async a=>{if(!a){n.remove(),o.classList.remove("has-mascot");return}const d=await a.snapshot(!0);document.documentElement.style.setProperty("--mascot-avatar",`url("${d}")`),document.documentElement.classList.add("has-mascot-avatar")}).catch(()=>{n.remove(),o.classList.remove("has-mascot")})}}let C=null;function Z(){var n,a;const e=document.getElementById("loading-title"),o=document.getElementById("progress-status"),t=document.getElementById("progress-detail");e&&(e.textContent="This browser cannot run the engine"),o&&(o.textContent="zero-tvm needs WebGPU with shader-f16. Chrome and Edge ship it; Safari from 26. The MoE models additionally need GPU subgroups (Chromium)."),t&&(t.textContent=""),(n=document.getElementById("start-dialog"))==null||n.remove(),(a=document.querySelector(".progress-track"))==null||a.remove()}async function Q(){var o;if(!("gpu"in navigator)){Z();return}j(),z(r);{const{accent:t,accentHi:n}=q(r),a=document.documentElement.style;a.setProperty("--accent",t),a.setProperty("--accent-hi",n),a.setProperty("--accent-2",n),a.setProperty("--accent-dim",`${t}22`),a.setProperty("--accent-tint",`${t}1f`)}{const t=document.getElementById("gate-mascot");t&&b(t,r).then(n=>{if(!n){t.remove();return}h&&r.moe&&n.setSpec(r,!1,h/(r.moe.experts+(r.sharedExpertIndex>=0?1:0)))}).catch(()=>t.remove())}R(()=>C),await G();const{isModelCached:e}=await x(async()=>{const{isModelCached:t}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:t}},__vite__mapDeps([0,1,2]));await e(r)?(o=document.getElementById("start-dialog"))==null||o.remove():await V(),await M()}Q().catch(e=>{console.error(e),f(`FATAL: ${e}`)});
|
chat-ui.css
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CHAT-UI β the conversational surface shared by zero-tvm.html (chat) and
|
| 2 |
+
* share.html (remote guest): palette, chat area, message bubbles, streaming
|
| 3 |
+
* markdown, actions, scroll FAB, composer. Moved VERBATIM out of
|
| 4 |
+
* zero-tvm.html's inline styles so the two pages cannot drift; the page-
|
| 5 |
+
* specific chrome (header, welcome, gate, overlay) stays inline per page.
|
| 6 |
+
* Companion module: src/zero-tvm/chat-ui.ts. */
|
| 7 |
+
|
| 8 |
+
/* ALIAS LAYER onto the family tokens (public/tokens.css), which must be
|
| 9 |
+
* linked BEFORE this file. The chat surface kept its own palette β a green
|
| 10 |
+
* accent on a different ground β so the instrument and the landing page
|
| 11 |
+
* looked like two products. Only the names this file does not share with
|
| 12 |
+
* tokens.css are redefined here; --bg, --border, --text, --accent and
|
| 13 |
+
* --mono come straight through.
|
| 14 |
+
*
|
| 15 |
+
* --muted is the exception and must be restated: this file uses it for the
|
| 16 |
+
* DIMMEST text, where the token set uses it for mid-tone. Letting the token
|
| 17 |
+
* value through would brighten every caption on the page. */
|
| 18 |
+
:root {
|
| 19 |
+
--panel: var(--surface);
|
| 20 |
+
--panel-2: var(--surface-2);
|
| 21 |
+
--border-2: var(--border-hi);
|
| 22 |
+
--text-dim: #a9b3c6;
|
| 23 |
+
--muted: var(--dim);
|
| 24 |
+
--accent-2: var(--accent-hi);
|
| 25 |
+
--accent-tint: var(--accent-dim);
|
| 26 |
+
--user-bubble: var(--surface-2);
|
| 27 |
+
--danger: var(--err);
|
| 28 |
+
--radius: 12px;
|
| 29 |
+
--composer-max: 760px;
|
| 30 |
+
--msg-max: 760px;
|
| 31 |
+
--font: var(--body);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* The browser's default focus ring is blue, which on this palette reads as
|
| 35 |
+
* a second selected state next to the amber one β in the model picker two
|
| 36 |
+
* chips looked chosen at once. Shared by the chat page and the rooms guest,
|
| 37 |
+
* matching landing.css. */
|
| 38 |
+
:focus-visible {
|
| 39 |
+
outline: 2px solid var(--accent);
|
| 40 |
+
outline-offset: 2px;
|
| 41 |
+
border-radius: var(--radius);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* βββββββββββ Chat area βββββββββββ */
|
| 45 |
+
/* share.html's guest view is a flex column, not zero-tvm.html's auto/1fr/auto
|
| 46 |
+
grid β without flex:1 this collapsed to ~56px on an empty room and the
|
| 47 |
+
composer floated near the top of a blank page. Scoped: on zero-tvm.html the
|
| 48 |
+
grid row sizes it and #guest-view does not exist. */
|
| 49 |
+
#guest-view .chat-main { flex: 1; min-height: 0; }
|
| 50 |
+
.chat-main {
|
| 51 |
+
overflow-y: auto;
|
| 52 |
+
scroll-behavior: smooth;
|
| 53 |
+
position: relative;
|
| 54 |
+
}
|
| 55 |
+
.chat-main::-webkit-scrollbar { width: 8px; }
|
| 56 |
+
.chat-main::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }
|
| 57 |
+
.chat-main::-webkit-scrollbar-track { background: transparent; }
|
| 58 |
+
|
| 59 |
+
.chat-inner {
|
| 60 |
+
max-width: var(--msg-max);
|
| 61 |
+
margin: 0 auto;
|
| 62 |
+
padding: 1.5rem 1rem 2rem;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
/* βββββββββββ Messages βββββββββββ */
|
| 67 |
+
.msg { margin-bottom: 1.75rem; animation: fade-in 0.2s ease; }
|
| 68 |
+
.msg:last-child { margin-bottom: 0.5rem; }
|
| 69 |
+
@keyframes fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
| 70 |
+
|
| 71 |
+
.msg.user {
|
| 72 |
+
display: flex;
|
| 73 |
+
justify-content: flex-end;
|
| 74 |
+
gap: 0.5rem;
|
| 75 |
+
}
|
| 76 |
+
.msg.user .bubble {
|
| 77 |
+
max-width: 80%;
|
| 78 |
+
background: var(--user-bubble);
|
| 79 |
+
border: 1px solid var(--border);
|
| 80 |
+
padding: 0.65rem 0.95rem;
|
| 81 |
+
border-radius: 16px 16px 4px 16px;
|
| 82 |
+
white-space: pre-wrap;
|
| 83 |
+
word-wrap: break-word;
|
| 84 |
+
line-height: 1.55;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.msg.ai .role {
|
| 88 |
+
display: flex;
|
| 89 |
+
align-items: center;
|
| 90 |
+
gap: 0.5rem;
|
| 91 |
+
font-size: 0.72rem;
|
| 92 |
+
color: var(--muted);
|
| 93 |
+
margin-bottom: 0.45rem;
|
| 94 |
+
font-weight: 500;
|
| 95 |
+
}
|
| 96 |
+
.msg.ai .role .role-name { color: var(--text-dim); }
|
| 97 |
+
.msg.ai .role .model-tag {
|
| 98 |
+
margin-left: auto;
|
| 99 |
+
font-size: 0.65rem;
|
| 100 |
+
color: var(--muted);
|
| 101 |
+
padding: 2px 7px;
|
| 102 |
+
border: 1px solid var(--border);
|
| 103 |
+
border-radius: 999px;
|
| 104 |
+
background: var(--panel);
|
| 105 |
+
font-variant-numeric: tabular-nums;
|
| 106 |
+
}
|
| 107 |
+
.msg.ai .role-dot {
|
| 108 |
+
width: 22px; height: 22px; border-radius: 7px;
|
| 109 |
+
/* Was a fixed violet, so every model's replies were badged in Phi-3's
|
| 110 |
+
colours. Both stops come from the model's own lane now. */
|
| 111 |
+
background: linear-gradient(135deg, var(--accent), var(--accent-hi));
|
| 112 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 113 |
+
font-size: 0.62rem; font-weight: 800; color: #000;
|
| 114 |
+
letter-spacing: -0.05em;
|
| 115 |
+
}
|
| 116 |
+
/* One still frame of the model's mascot, captured once at boot and reused
|
| 117 |
+
by every reply. The letter stays as the fallback for browsers that gave
|
| 118 |
+
us no mascot to capture. */
|
| 119 |
+
.has-mascot-avatar .msg.ai .role-dot {
|
| 120 |
+
background: var(--mascot-avatar) center/cover no-repeat,
|
| 121 |
+
linear-gradient(135deg, var(--accent), var(--accent-hi));
|
| 122 |
+
font-size: 0;
|
| 123 |
+
}
|
| 124 |
+
.msg.ai .body {
|
| 125 |
+
color: var(--text);
|
| 126 |
+
word-wrap: break-word;
|
| 127 |
+
line-height: 1.65;
|
| 128 |
+
}
|
| 129 |
+
.msg.ai .body > *:first-child { margin-top: 0; }
|
| 130 |
+
.msg.ai .body > *:last-child { margin-bottom: 0; }
|
| 131 |
+
.msg.ai .body p { margin: 0 0 0.7rem; }
|
| 132 |
+
.msg.ai .body ul, .msg.ai .body ol {
|
| 133 |
+
margin: 0 0 0.7rem;
|
| 134 |
+
padding-left: 1.4rem;
|
| 135 |
+
}
|
| 136 |
+
.msg.ai .body li { margin-bottom: 0.25rem; }
|
| 137 |
+
.msg.ai .body li > p { margin: 0; }
|
| 138 |
+
.msg.ai .body h1, .msg.ai .body h2, .msg.ai .body h3,
|
| 139 |
+
.msg.ai .body h4, .msg.ai .body h5, .msg.ai .body h6 {
|
| 140 |
+
font-weight: 600;
|
| 141 |
+
letter-spacing: -0.01em;
|
| 142 |
+
margin: 1rem 0 0.45rem;
|
| 143 |
+
line-height: 1.3;
|
| 144 |
+
color: #fff;
|
| 145 |
+
}
|
| 146 |
+
.msg.ai .body h1 { font-size: 1.25rem; }
|
| 147 |
+
.msg.ai .body h2 { font-size: 1.12rem; }
|
| 148 |
+
.msg.ai .body h3 { font-size: 1.02rem; }
|
| 149 |
+
.msg.ai .body h4, .msg.ai .body h5, .msg.ai .body h6 { font-size: 0.95rem; }
|
| 150 |
+
.msg.ai .body code {
|
| 151 |
+
font-family: var(--mono);
|
| 152 |
+
font-size: 0.85em;
|
| 153 |
+
background: var(--panel-2);
|
| 154 |
+
padding: 1px 5px;
|
| 155 |
+
border-radius: 4px;
|
| 156 |
+
border: 1px solid var(--border);
|
| 157 |
+
}
|
| 158 |
+
.msg.ai .body blockquote {
|
| 159 |
+
border-left: 3px solid var(--border-2);
|
| 160 |
+
padding: 0.1rem 0 0.1rem 0.85rem;
|
| 161 |
+
color: var(--text-dim);
|
| 162 |
+
margin: 0 0 0.7rem;
|
| 163 |
+
}
|
| 164 |
+
.msg.ai .body hr {
|
| 165 |
+
border: 0;
|
| 166 |
+
border-top: 1px solid var(--border);
|
| 167 |
+
margin: 1rem 0;
|
| 168 |
+
}
|
| 169 |
+
.msg.ai .body a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; }
|
| 170 |
+
.msg.ai .body strong { color: #fff; font-weight: 600; }
|
| 171 |
+
|
| 172 |
+
/* Fenced code block β top bar with lang pill + copy button */
|
| 173 |
+
.msg.ai .body .code-block {
|
| 174 |
+
margin: 0.7rem 0;
|
| 175 |
+
border-radius: 10px;
|
| 176 |
+
border: 1px solid var(--border);
|
| 177 |
+
background: var(--panel);
|
| 178 |
+
overflow: hidden;
|
| 179 |
+
}
|
| 180 |
+
.msg.ai .body .code-head {
|
| 181 |
+
display: flex;
|
| 182 |
+
align-items: center;
|
| 183 |
+
justify-content: space-between;
|
| 184 |
+
padding: 0.4rem 0.6rem 0.4rem 0.75rem;
|
| 185 |
+
background: var(--panel-2);
|
| 186 |
+
border-bottom: 1px solid var(--border);
|
| 187 |
+
font-size: 0.68rem;
|
| 188 |
+
color: var(--muted);
|
| 189 |
+
}
|
| 190 |
+
.msg.ai .body .code-lang {
|
| 191 |
+
text-transform: uppercase;
|
| 192 |
+
letter-spacing: 0.08em;
|
| 193 |
+
font-weight: 600;
|
| 194 |
+
}
|
| 195 |
+
.msg.ai .body .code-copy {
|
| 196 |
+
display: inline-flex;
|
| 197 |
+
align-items: center;
|
| 198 |
+
gap: 4px;
|
| 199 |
+
padding: 3px 8px;
|
| 200 |
+
border-radius: 6px;
|
| 201 |
+
color: var(--text-dim);
|
| 202 |
+
font-size: 0.68rem;
|
| 203 |
+
transition: background 0.15s, color 0.15s;
|
| 204 |
+
}
|
| 205 |
+
.msg.ai .body .code-copy:hover { background: rgba(255,255,255,0.06); color: var(--text); }
|
| 206 |
+
.msg.ai .body .code-copy.copied { color: var(--accent); }
|
| 207 |
+
.msg.ai .body .code-copy .icon { width: 11px; height: 11px; stroke-width: 2.3; }
|
| 208 |
+
.msg.ai .body .code-block pre {
|
| 209 |
+
margin: 0;
|
| 210 |
+
padding: 0.75rem 0.9rem;
|
| 211 |
+
overflow-x: auto;
|
| 212 |
+
background: none;
|
| 213 |
+
border: none;
|
| 214 |
+
}
|
| 215 |
+
.msg.ai .body .code-block pre code {
|
| 216 |
+
background: none;
|
| 217 |
+
border: none;
|
| 218 |
+
padding: 0;
|
| 219 |
+
font-size: 0.82rem;
|
| 220 |
+
line-height: 1.5;
|
| 221 |
+
color: var(--text);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.msg.ai .cursor {
|
| 225 |
+
display: inline-block;
|
| 226 |
+
width: 7px;
|
| 227 |
+
height: 1.05em;
|
| 228 |
+
background: var(--accent);
|
| 229 |
+
margin-left: 2px;
|
| 230 |
+
vertical-align: text-bottom;
|
| 231 |
+
border-radius: 1px;
|
| 232 |
+
animation: blink 1.1s steps(2) infinite;
|
| 233 |
+
opacity: 0.9;
|
| 234 |
+
}
|
| 235 |
+
@keyframes blink { 50% { opacity: 0.1; } }
|
| 236 |
+
|
| 237 |
+
/* Thinking indicator β 3 dots that wave while prefill runs */
|
| 238 |
+
.thinking {
|
| 239 |
+
display: inline-flex;
|
| 240 |
+
align-items: center;
|
| 241 |
+
gap: 4px;
|
| 242 |
+
padding: 2px 0;
|
| 243 |
+
}
|
| 244 |
+
.thinking span {
|
| 245 |
+
width: 6px; height: 6px;
|
| 246 |
+
border-radius: 50%;
|
| 247 |
+
background: var(--muted);
|
| 248 |
+
animation: wave 1.2s ease-in-out infinite;
|
| 249 |
+
}
|
| 250 |
+
.thinking span:nth-child(2) { animation-delay: 0.15s; }
|
| 251 |
+
.thinking span:nth-child(3) { animation-delay: 0.3s; }
|
| 252 |
+
@keyframes wave {
|
| 253 |
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
| 254 |
+
30% { transform: translateY(-3px); opacity: 1; }
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.msg .actions {
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
gap: 0.15rem;
|
| 261 |
+
margin-top: 0.55rem;
|
| 262 |
+
opacity: 0;
|
| 263 |
+
transition: opacity 0.2s;
|
| 264 |
+
}
|
| 265 |
+
.msg:hover .actions,
|
| 266 |
+
.msg:focus-within .actions,
|
| 267 |
+
.msg .actions:focus-within { opacity: 1; }
|
| 268 |
+
.msg.ai .actions { margin-left: -0.35rem; }
|
| 269 |
+
.msg.ai .actions .msg-stats {
|
| 270 |
+
margin-left: auto;
|
| 271 |
+
font-size: 0.66rem;
|
| 272 |
+
color: var(--muted);
|
| 273 |
+
font-variant-numeric: tabular-nums;
|
| 274 |
+
padding-right: 0.2rem;
|
| 275 |
+
}
|
| 276 |
+
.msg.user .actions { justify-content: flex-end; }
|
| 277 |
+
|
| 278 |
+
/* Reply cut short (context window full, or the user hit Stop). Unlike
|
| 279 |
+
.actions this is always visible β a truncated answer must never look
|
| 280 |
+
like a finished one. */
|
| 281 |
+
.msg.ai .truncation {
|
| 282 |
+
display: flex;
|
| 283 |
+
align-items: center;
|
| 284 |
+
gap: 0.6rem;
|
| 285 |
+
flex-wrap: wrap;
|
| 286 |
+
margin-top: 0.7rem;
|
| 287 |
+
padding: 0.45rem 0.7rem;
|
| 288 |
+
border: 1px solid var(--border-2);
|
| 289 |
+
border-left: 2px solid var(--accent-2);
|
| 290 |
+
border-radius: 8px;
|
| 291 |
+
background: var(--panel-2);
|
| 292 |
+
font-size: 0.72rem;
|
| 293 |
+
line-height: 1.45;
|
| 294 |
+
color: var(--text-dim);
|
| 295 |
+
}
|
| 296 |
+
.truncation-btn {
|
| 297 |
+
padding: 3px 11px;
|
| 298 |
+
border-radius: 6px;
|
| 299 |
+
font-size: 0.7rem;
|
| 300 |
+
font-weight: 600;
|
| 301 |
+
color: #000;
|
| 302 |
+
background: var(--accent);
|
| 303 |
+
}
|
| 304 |
+
.truncation-btn:hover { background: var(--accent-2); }
|
| 305 |
+
.truncation-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
| 306 |
+
.action-btn {
|
| 307 |
+
display: inline-flex;
|
| 308 |
+
align-items: center;
|
| 309 |
+
gap: 4px;
|
| 310 |
+
padding: 4px 8px;
|
| 311 |
+
border-radius: 6px;
|
| 312 |
+
font-size: 0.7rem;
|
| 313 |
+
color: var(--muted);
|
| 314 |
+
transition: background 0.15s, color 0.15s;
|
| 315 |
+
}
|
| 316 |
+
.action-btn:hover { background: var(--panel-2); color: var(--text); }
|
| 317 |
+
.action-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
| 318 |
+
.action-btn.copied { color: var(--accent); }
|
| 319 |
+
.action-btn .icon { width: 13px; height: 13px; }
|
| 320 |
+
|
| 321 |
+
/* βββββββββββ Scroll-to-bottom FAB βββββββββββ */
|
| 322 |
+
.scroll-fab {
|
| 323 |
+
position: absolute;
|
| 324 |
+
bottom: 1rem;
|
| 325 |
+
left: 50%;
|
| 326 |
+
transform: translateX(-50%);
|
| 327 |
+
width: 34px;
|
| 328 |
+
height: 34px;
|
| 329 |
+
border-radius: 50%;
|
| 330 |
+
background: var(--panel-2);
|
| 331 |
+
border: 1px solid var(--border-2);
|
| 332 |
+
display: flex;
|
| 333 |
+
align-items: center;
|
| 334 |
+
justify-content: center;
|
| 335 |
+
color: var(--text-dim);
|
| 336 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
| 337 |
+
opacity: 0;
|
| 338 |
+
pointer-events: none;
|
| 339 |
+
transition: opacity 0.2s, transform 0.2s;
|
| 340 |
+
z-index: 5;
|
| 341 |
+
}
|
| 342 |
+
.scroll-fab.visible { opacity: 1; pointer-events: auto; visibility: visible; }
|
| 343 |
+
.scroll-fab:hover { background: var(--panel); color: var(--text); }
|
| 344 |
+
|
| 345 |
+
/* βββββββββββ Composer βββββββββββ */
|
| 346 |
+
.composer-wrap {
|
| 347 |
+
padding: 0.75rem 1rem 1rem;
|
| 348 |
+
border-top: 1px solid var(--border);
|
| 349 |
+
background: var(--bg);
|
| 350 |
+
}
|
| 351 |
+
.composer {
|
| 352 |
+
max-width: var(--composer-max);
|
| 353 |
+
margin: 0 auto;
|
| 354 |
+
display: flex;
|
| 355 |
+
align-items: flex-end;
|
| 356 |
+
gap: 0.5rem;
|
| 357 |
+
background: var(--panel);
|
| 358 |
+
border: 1px solid var(--border-2);
|
| 359 |
+
border-radius: 14px;
|
| 360 |
+
padding: 0.5rem 0.5rem 0.5rem 0.9rem;
|
| 361 |
+
transition: border-color 0.15s, background 0.15s;
|
| 362 |
+
}
|
| 363 |
+
/* border-color used to restate the UNFOCUSED border β a focus style that
|
| 364 |
+
cancelled itself, leaving a 1.07:1 background shift as the only cue on
|
| 365 |
+
the product's main control. The accent is the ring every other control
|
| 366 |
+
already uses. */
|
| 367 |
+
.composer:focus-within {
|
| 368 |
+
border-color: var(--accent);
|
| 369 |
+
background: var(--panel-2);
|
| 370 |
+
}
|
| 371 |
+
.composer textarea {
|
| 372 |
+
flex: 1;
|
| 373 |
+
background: none;
|
| 374 |
+
border: none;
|
| 375 |
+
outline: none;
|
| 376 |
+
color: var(--text);
|
| 377 |
+
font: inherit;
|
| 378 |
+
resize: none;
|
| 379 |
+
padding: 0.4rem 0;
|
| 380 |
+
max-height: 200px;
|
| 381 |
+
line-height: 1.5;
|
| 382 |
+
}
|
| 383 |
+
.composer textarea::placeholder { color: var(--muted); }
|
| 384 |
+
.composer textarea:disabled { cursor: not-allowed; }
|
| 385 |
+
|
| 386 |
+
.composer-btn {
|
| 387 |
+
display: flex;
|
| 388 |
+
align-items: center;
|
| 389 |
+
justify-content: center;
|
| 390 |
+
width: 34px;
|
| 391 |
+
height: 34px;
|
| 392 |
+
border-radius: 10px;
|
| 393 |
+
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
| 394 |
+
flex-shrink: 0;
|
| 395 |
+
}
|
| 396 |
+
.composer-btn[hidden] { display: none; }
|
| 397 |
+
.composer-btn[data-variant="send"] {
|
| 398 |
+
background: var(--accent);
|
| 399 |
+
color: #000;
|
| 400 |
+
}
|
| 401 |
+
.composer-btn[data-variant="send"]:hover:not(:disabled) { background: var(--accent-2); }
|
| 402 |
+
.composer-btn[data-variant="send"]:disabled {
|
| 403 |
+
background: var(--border);
|
| 404 |
+
color: var(--muted);
|
| 405 |
+
cursor: not-allowed;
|
| 406 |
+
}
|
| 407 |
+
.composer-btn[data-variant="stop"] {
|
| 408 |
+
background: var(--panel-2);
|
| 409 |
+
color: var(--text);
|
| 410 |
+
border: 1px solid var(--border-2);
|
| 411 |
+
}
|
| 412 |
+
.composer-btn[data-variant="stop"]:hover { background: #222228; }
|
| 413 |
+
|
| 414 |
+
.composer-hint {
|
| 415 |
+
max-width: var(--composer-max);
|
| 416 |
+
margin: 0.4rem auto 0;
|
| 417 |
+
text-align: center;
|
| 418 |
+
font-size: 0.68rem;
|
| 419 |
+
color: var(--muted);
|
| 420 |
+
}
|
| 421 |
+
.composer-hint kbd {
|
| 422 |
+
background: var(--panel-2);
|
| 423 |
+
border: 1px solid var(--border);
|
| 424 |
+
padding: 1px 5px;
|
| 425 |
+
border-radius: 4px;
|
| 426 |
+
font-family: var(--mono);
|
| 427 |
+
font-size: 0.62rem;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
/* βββββββββββ The Canvas (model-written code, actually run) βββββββββββ */
|
| 432 |
+
/* Run button beside copy; the lang pill pushes both right so the pair
|
| 433 |
+
stays grouped even though .code-head is space-between. */
|
| 434 |
+
.msg.ai .body .code-head .code-lang { margin-right: auto; }
|
| 435 |
+
.msg.ai .body .code-head { gap: 6px; }
|
| 436 |
+
.msg.ai .body .code-run {
|
| 437 |
+
display: inline-flex;
|
| 438 |
+
align-items: center;
|
| 439 |
+
gap: 4px;
|
| 440 |
+
padding: 3px 8px;
|
| 441 |
+
border-radius: 6px;
|
| 442 |
+
color: var(--accent);
|
| 443 |
+
font-size: 0.68rem;
|
| 444 |
+
font-weight: 600;
|
| 445 |
+
transition: background 0.15s, color 0.15s;
|
| 446 |
+
}
|
| 447 |
+
.msg.ai .body .code-run:hover { background: var(--accent-tint); }
|
| 448 |
+
.msg.ai .body .code-run .icon { width: 11px; height: 11px; stroke-width: 2.3; }
|
| 449 |
+
|
| 450 |
+
#code-canvas {
|
| 451 |
+
position: fixed;
|
| 452 |
+
inset: 0;
|
| 453 |
+
z-index: 120;
|
| 454 |
+
display: flex;
|
| 455 |
+
align-items: center;
|
| 456 |
+
justify-content: center;
|
| 457 |
+
padding: 4vh 4vw;
|
| 458 |
+
background: rgba(5, 7, 13, 0.72);
|
| 459 |
+
backdrop-filter: blur(5px);
|
| 460 |
+
}
|
| 461 |
+
.canvas-frame {
|
| 462 |
+
width: min(940px, 100%);
|
| 463 |
+
height: min(660px, 100%);
|
| 464 |
+
display: flex;
|
| 465 |
+
flex-direction: column;
|
| 466 |
+
background: var(--panel);
|
| 467 |
+
border: 1px solid var(--border-2);
|
| 468 |
+
border-radius: 12px;
|
| 469 |
+
overflow: hidden;
|
| 470 |
+
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);
|
| 471 |
+
}
|
| 472 |
+
.canvas-head {
|
| 473 |
+
display: flex;
|
| 474 |
+
align-items: center;
|
| 475 |
+
justify-content: space-between;
|
| 476 |
+
padding: 8px 10px 8px 14px;
|
| 477 |
+
border-bottom: 1px solid var(--border);
|
| 478 |
+
font-size: 0.7rem;
|
| 479 |
+
color: var(--muted);
|
| 480 |
+
text-transform: uppercase;
|
| 481 |
+
letter-spacing: 0.08em;
|
| 482 |
+
}
|
| 483 |
+
.canvas-close {
|
| 484 |
+
padding: 3px 9px;
|
| 485 |
+
border-radius: 6px;
|
| 486 |
+
color: var(--text-dim);
|
| 487 |
+
font-size: 0.7rem;
|
| 488 |
+
transition: background 0.15s, color 0.15s;
|
| 489 |
+
}
|
| 490 |
+
.canvas-close:hover { background: var(--panel-2); color: var(--text); }
|
| 491 |
+
/* The iframe ground is white on purpose: model-written pages assume a
|
| 492 |
+
default (light) document, and an unstyled preview should look like one. */
|
| 493 |
+
.canvas-view { flex: 1; width: 100%; border: 0; background: #fff; }
|
| 494 |
+
|
| 495 |
+
/* βββββββββββ Command feedback + reply settle βββββββββββ */
|
| 496 |
+
/* A quiet system line in the message column (slash-command feedback) β
|
| 497 |
+
styled as stage direction, not as a conversation turn. */
|
| 498 |
+
.sys-note {
|
| 499 |
+
margin: 0.6rem auto 1rem;
|
| 500 |
+
max-width: 80%;
|
| 501 |
+
text-align: center;
|
| 502 |
+
font-family: var(--mono);
|
| 503 |
+
font-size: 0.68rem;
|
| 504 |
+
letter-spacing: 0.04em;
|
| 505 |
+
color: var(--muted);
|
| 506 |
+
}
|
| 507 |
+
/* A finished reply settles with a brief accent wash. */
|
| 508 |
+
.msg.ai.msg-done { animation: msgDone 0.8s ease; border-radius: 10px; }
|
| 509 |
+
@keyframes msgDone {
|
| 510 |
+
0% { background: var(--accent-tint); }
|
| 511 |
+
100% { background: transparent; }
|
| 512 |
+
}
|
| 513 |
+
@media (prefers-reduced-motion: reduce) { .msg.ai.msg-done { animation: none; } }
|
| 514 |
+
|
| 515 |
+
/* Reduced motion: the message surface holds still too β instant scroll,
|
| 516 |
+
no entrance fade, steady cursor and thinking dots (they stay VISIBLE;
|
| 517 |
+
only the movement goes). */
|
| 518 |
+
@media (prefers-reduced-motion: reduce) {
|
| 519 |
+
.chat-main { scroll-behavior: auto; }
|
| 520 |
+
.msg { animation: none; }
|
| 521 |
+
.msg.ai .cursor { animation: none; }
|
| 522 |
+
.thinking span { animation: none; opacity: 0.6; }
|
| 523 |
+
}
|
docs.html
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Docs β Zero-TVM</title>
|
| 7 |
+
<meta name="description" content="Zero-TVM docs: shader catalog, decode pipeline, weight loader, URL flags, benchmarks. LLMs up to a 35B MoE in the browser on 10 hand-written WGSL kernel roles.">
|
| 8 |
+
<meta name="theme-color" content="#0a0a0c">
|
| 9 |
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
| 10 |
+
<meta property="og:type" content="article">
|
| 11 |
+
<meta property="og:url" content="https://zerotvm.com/docs.html">
|
| 12 |
+
<meta property="og:title" content="Docs β Zero-TVM">
|
| 13 |
+
<meta property="og:description" content="Zero-TVM docs: shader catalog, decode pipeline, weight loader, URL flags, benchmarks. LLMs up to a 35B MoE in the browser on 10 hand-written WGSL kernel roles.">
|
| 14 |
+
<meta property="og:image" content="https://zerotvm.com/og.png">
|
| 15 |
+
<meta name="twitter:card" content="summary_large_image">
|
| 16 |
+
<meta name="twitter:title" content="Docs β Zero-TVM">
|
| 17 |
+
<meta name="twitter:description" content="Zero-TVM docs: shader catalog, decode pipeline, weight loader, URL flags, benchmarks. LLMs up to a 35B MoE in the browser on 10 hand-written WGSL kernel roles.">
|
| 18 |
+
<meta name="twitter:image" content="https://zerotvm.com/og.png">
|
| 19 |
+
<style>
|
| 20 |
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
| 21 |
+
|
| 22 |
+
:root {
|
| 23 |
+
--bg: #0a0a0c;
|
| 24 |
+
--surface: #111114;
|
| 25 |
+
--surface2: #18181c;
|
| 26 |
+
--border: #1e1e24;
|
| 27 |
+
--accent: #6c63ff;
|
| 28 |
+
--green: #00d4aa;
|
| 29 |
+
--yellow: #f5c542;
|
| 30 |
+
--red: #ff5f57;
|
| 31 |
+
--text: #e8e8ec;
|
| 32 |
+
--muted: #666672;
|
| 33 |
+
--dim: #3a3a44;
|
| 34 |
+
--sidebar-w: 260px;
|
| 35 |
+
--topbar-h: 52px;
|
| 36 |
+
--code-bg: #0d0d10;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
html { scroll-behavior: smooth; }
|
| 40 |
+
|
| 41 |
+
body {
|
| 42 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 43 |
+
background: var(--bg);
|
| 44 |
+
color: var(--text);
|
| 45 |
+
line-height: 1.65;
|
| 46 |
+
display: flex;
|
| 47 |
+
flex-direction: column;
|
| 48 |
+
min-height: 100vh;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* ββ TOPBAR ββ */
|
| 52 |
+
.topbar {
|
| 53 |
+
height: var(--topbar-h);
|
| 54 |
+
background: var(--surface);
|
| 55 |
+
border-bottom: 1px solid var(--border);
|
| 56 |
+
display: flex;
|
| 57 |
+
align-items: center;
|
| 58 |
+
padding: 0 1.5rem;
|
| 59 |
+
gap: 1.5rem;
|
| 60 |
+
position: fixed;
|
| 61 |
+
top: 0; left: 0; right: 0;
|
| 62 |
+
z-index: 100;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.logo {
|
| 66 |
+
font-weight: 800;
|
| 67 |
+
font-size: 0.95rem;
|
| 68 |
+
letter-spacing: -0.03em;
|
| 69 |
+
color: #fff;
|
| 70 |
+
text-decoration: none;
|
| 71 |
+
}
|
| 72 |
+
.logo span { color: var(--accent); }
|
| 73 |
+
|
| 74 |
+
.topbar-sep { color: var(--border); }
|
| 75 |
+
.topbar-title { font-size: 0.82rem; color: var(--muted); }
|
| 76 |
+
|
| 77 |
+
.topbar-nav {
|
| 78 |
+
margin-left: auto;
|
| 79 |
+
display: flex;
|
| 80 |
+
gap: 1.25rem;
|
| 81 |
+
align-items: center;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.topbar-nav a {
|
| 85 |
+
font-size: 0.8rem;
|
| 86 |
+
color: var(--muted);
|
| 87 |
+
text-decoration: none;
|
| 88 |
+
transition: color 0.15s;
|
| 89 |
+
}
|
| 90 |
+
.topbar-nav a:hover { color: var(--text); }
|
| 91 |
+
|
| 92 |
+
.topbar-cta {
|
| 93 |
+
background: var(--accent);
|
| 94 |
+
color: #fff !important;
|
| 95 |
+
padding: 0.3rem 0.85rem;
|
| 96 |
+
border-radius: 7px;
|
| 97 |
+
font-weight: 600 !important;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* ββ LAYOUT ββ */
|
| 101 |
+
.layout {
|
| 102 |
+
display: flex;
|
| 103 |
+
flex: 1;
|
| 104 |
+
padding-top: var(--topbar-h);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* ββ SIDEBAR ββ */
|
| 108 |
+
aside {
|
| 109 |
+
width: var(--sidebar-w);
|
| 110 |
+
min-width: var(--sidebar-w);
|
| 111 |
+
background: var(--surface);
|
| 112 |
+
border-right: 1px solid var(--border);
|
| 113 |
+
position: fixed;
|
| 114 |
+
top: var(--topbar-h);
|
| 115 |
+
bottom: 0;
|
| 116 |
+
left: 0;
|
| 117 |
+
overflow-y: auto;
|
| 118 |
+
padding: 1.5rem 0;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
aside::-webkit-scrollbar { width: 4px; }
|
| 122 |
+
aside::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
| 123 |
+
|
| 124 |
+
.sidebar-section {
|
| 125 |
+
padding: 0 1rem 1rem;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.sidebar-section-label {
|
| 129 |
+
font-size: 0.6rem;
|
| 130 |
+
font-weight: 700;
|
| 131 |
+
letter-spacing: 0.1em;
|
| 132 |
+
text-transform: uppercase;
|
| 133 |
+
color: var(--muted);
|
| 134 |
+
padding: 0.5rem 0.5rem 0.4rem;
|
| 135 |
+
display: block;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.sidebar-link {
|
| 139 |
+
display: block;
|
| 140 |
+
padding: 0.35rem 0.5rem;
|
| 141 |
+
font-size: 0.78rem;
|
| 142 |
+
color: var(--muted);
|
| 143 |
+
text-decoration: none;
|
| 144 |
+
border-radius: 6px;
|
| 145 |
+
transition: background 0.1s, color 0.1s;
|
| 146 |
+
border-left: 2px solid transparent;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.sidebar-link:hover { color: var(--text); background: var(--surface2); }
|
| 150 |
+
.sidebar-link.active { color: var(--accent); border-left-color: var(--accent); background: #6c63ff0d; }
|
| 151 |
+
.sidebar-link.sub { padding-left: 1.25rem; font-size: 0.73rem; }
|
| 152 |
+
|
| 153 |
+
/* ββ MAIN CONTENT ββ */
|
| 154 |
+
main {
|
| 155 |
+
margin-left: var(--sidebar-w);
|
| 156 |
+
flex: 1;
|
| 157 |
+
padding: 3.5rem 3rem 6rem;
|
| 158 |
+
max-width: 900px;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/* ββ TYPOGRAPHY ββ */
|
| 162 |
+
h1 {
|
| 163 |
+
font-size: 2rem;
|
| 164 |
+
font-weight: 800;
|
| 165 |
+
letter-spacing: -0.03em;
|
| 166 |
+
margin-bottom: 0.5rem;
|
| 167 |
+
line-height: 1.2;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
h2 {
|
| 171 |
+
font-size: 1.35rem;
|
| 172 |
+
font-weight: 700;
|
| 173 |
+
letter-spacing: -0.02em;
|
| 174 |
+
margin-top: 3rem;
|
| 175 |
+
margin-bottom: 0.75rem;
|
| 176 |
+
padding-top: 2rem;
|
| 177 |
+
border-top: 1px solid var(--border);
|
| 178 |
+
scroll-margin-top: calc(var(--topbar-h) + 1rem);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
h2:first-of-type { border-top: none; margin-top: 1.5rem; padding-top: 0; }
|
| 182 |
+
|
| 183 |
+
h3 {
|
| 184 |
+
font-size: 1rem;
|
| 185 |
+
font-weight: 700;
|
| 186 |
+
margin-top: 1.75rem;
|
| 187 |
+
margin-bottom: 0.5rem;
|
| 188 |
+
color: var(--text);
|
| 189 |
+
scroll-margin-top: calc(var(--topbar-h) + 1rem);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
h4 {
|
| 193 |
+
font-size: 0.88rem;
|
| 194 |
+
font-weight: 700;
|
| 195 |
+
margin-top: 1.25rem;
|
| 196 |
+
margin-bottom: 0.35rem;
|
| 197 |
+
color: var(--muted);
|
| 198 |
+
text-transform: uppercase;
|
| 199 |
+
letter-spacing: 0.05em;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
p {
|
| 203 |
+
font-size: 0.9rem;
|
| 204 |
+
color: #b0b0bc;
|
| 205 |
+
margin-bottom: 1rem;
|
| 206 |
+
line-height: 1.75;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
a { color: var(--accent); text-decoration: none; }
|
| 210 |
+
a:hover { text-decoration: underline; }
|
| 211 |
+
|
| 212 |
+
ul, ol {
|
| 213 |
+
margin: 0.5rem 0 1rem 1.5rem;
|
| 214 |
+
font-size: 0.88rem;
|
| 215 |
+
color: #b0b0bc;
|
| 216 |
+
line-height: 1.8;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
li { margin-bottom: 0.2rem; }
|
| 220 |
+
|
| 221 |
+
strong { color: var(--text); font-weight: 600; }
|
| 222 |
+
em { color: var(--yellow); font-style: normal; }
|
| 223 |
+
|
| 224 |
+
/* ββ LEAD / SUBTITLE ββ */
|
| 225 |
+
.lead {
|
| 226 |
+
font-size: 1.05rem;
|
| 227 |
+
color: var(--muted);
|
| 228 |
+
margin-bottom: 2rem;
|
| 229 |
+
line-height: 1.7;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* ββ PILL BADGES ββ */
|
| 233 |
+
.pill {
|
| 234 |
+
display: inline-flex;
|
| 235 |
+
align-items: center;
|
| 236 |
+
gap: 0.25rem;
|
| 237 |
+
padding: 0.15rem 0.5rem;
|
| 238 |
+
border-radius: 999px;
|
| 239 |
+
font-size: 0.65rem;
|
| 240 |
+
font-weight: 600;
|
| 241 |
+
border: 1px solid;
|
| 242 |
+
vertical-align: middle;
|
| 243 |
+
}
|
| 244 |
+
.pill.green { background: #00d4aa12; color: var(--green); border-color: #00d4aa30; }
|
| 245 |
+
.pill.purple { background: #6c63ff12; color: var(--accent); border-color: #6c63ff30; }
|
| 246 |
+
.pill.yellow { background: #f5c54212; color: var(--yellow); border-color: #f5c54230; }
|
| 247 |
+
.pill.red { background: #ff5f5712; color: var(--red); border-color: #ff5f5730; }
|
| 248 |
+
|
| 249 |
+
/* ββ CODE ββ */
|
| 250 |
+
code {
|
| 251 |
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
| 252 |
+
font-size: 0.8em;
|
| 253 |
+
background: var(--surface2);
|
| 254 |
+
border: 1px solid var(--border);
|
| 255 |
+
padding: 0.1em 0.4em;
|
| 256 |
+
border-radius: 4px;
|
| 257 |
+
color: #c8c8e0;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
pre {
|
| 261 |
+
background: var(--code-bg);
|
| 262 |
+
border: 1px solid var(--border);
|
| 263 |
+
border-radius: 10px;
|
| 264 |
+
padding: 1.25rem 1.5rem;
|
| 265 |
+
overflow-x: auto;
|
| 266 |
+
margin: 1rem 0 1.5rem;
|
| 267 |
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
| 268 |
+
font-size: 0.78rem;
|
| 269 |
+
line-height: 1.65;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
pre code {
|
| 273 |
+
background: none;
|
| 274 |
+
border: none;
|
| 275 |
+
padding: 0;
|
| 276 |
+
font-size: inherit;
|
| 277 |
+
color: #c8c8e0;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/* syntax highlight classes */
|
| 281 |
+
.kw { color: #c792ea; }
|
| 282 |
+
.fn { color: #82aaff; }
|
| 283 |
+
.str { color: #c3e88d; }
|
| 284 |
+
.num { color: #f78c6c; }
|
| 285 |
+
.cm { color: #546e7a; font-style: italic; }
|
| 286 |
+
.ty { color: #ffcb6b; }
|
| 287 |
+
.op { color: #89ddff; }
|
| 288 |
+
|
| 289 |
+
/* ββ PRE HEADER (filename label) ββ */
|
| 290 |
+
.code-block { margin: 1rem 0 1.5rem; }
|
| 291 |
+
.code-label {
|
| 292 |
+
background: var(--surface2);
|
| 293 |
+
border: 1px solid var(--border);
|
| 294 |
+
border-bottom: none;
|
| 295 |
+
border-radius: 8px 8px 0 0;
|
| 296 |
+
padding: 0.4rem 1rem;
|
| 297 |
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
| 298 |
+
font-size: 0.7rem;
|
| 299 |
+
color: var(--muted);
|
| 300 |
+
display: flex;
|
| 301 |
+
align-items: center;
|
| 302 |
+
gap: 0.5rem;
|
| 303 |
+
}
|
| 304 |
+
.code-block pre { margin: 0; border-radius: 0 0 8px 8px; }
|
| 305 |
+
|
| 306 |
+
/* ββ CALLOUTS ββ */
|
| 307 |
+
.callout {
|
| 308 |
+
border-radius: 10px;
|
| 309 |
+
padding: 1rem 1.25rem;
|
| 310 |
+
margin: 1.25rem 0;
|
| 311 |
+
font-size: 0.85rem;
|
| 312 |
+
line-height: 1.65;
|
| 313 |
+
border-left: 3px solid;
|
| 314 |
+
}
|
| 315 |
+
.callout p { margin: 0; color: inherit; font-size: inherit; }
|
| 316 |
+
.callout.info { background: #6c63ff0d; border-color: var(--accent); color: #a0a0d0; }
|
| 317 |
+
.callout.tip { background: #00d4aa0d; border-color: var(--green); color: #80c8bc; }
|
| 318 |
+
.callout.warning { background: #f5c5420d; border-color: var(--yellow); color: #c0a060; }
|
| 319 |
+
.callout.danger { background: #ff5f570d; border-color: var(--red); color: #c08080; }
|
| 320 |
+
|
| 321 |
+
.callout-title { font-weight: 700; margin-bottom: 0.3rem; color: inherit; }
|
| 322 |
+
|
| 323 |
+
/* ββ NARROW SCREENS ββ
|
| 324 |
+
This page shipped with NO media query at all, behind a 260px fixed
|
| 325 |
+
sidebar. At 390px that left main a 130px border-box and, after its 3rem
|
| 326 |
+
side padding, about 34px of readable text β with the sidebar covering
|
| 327 |
+
two thirds of the screen. It is nav-linked from every page and sits in
|
| 328 |
+
the sitemap, so a shared link opened on a phone landed on that. */
|
| 329 |
+
@media (max-width: 900px) {
|
| 330 |
+
:root { --sidebar-w: 0px; }
|
| 331 |
+
aside {
|
| 332 |
+
position: static; width: auto; min-width: 0; height: auto;
|
| 333 |
+
border-right: 0; border-bottom: 1px solid var(--border);
|
| 334 |
+
max-height: 40vh; overflow-y: auto;
|
| 335 |
+
}
|
| 336 |
+
main { margin-left: 0; padding: 2rem 1.1rem 4rem; max-width: none; }
|
| 337 |
+
/* THE FLEX CONTEXT IS .layout, NOT body. The first version of this
|
| 338 |
+
query reblocked body β which was already stacked β and left .layout
|
| 339 |
+
a flex row, so the sidebar stayed a 124px side column of wrapped
|
| 340 |
+
links beside ~230px of text. Reviewed at 390px; this is the line
|
| 341 |
+
that actually stacks them. */
|
| 342 |
+
.layout { display: block; }
|
| 343 |
+
.topbar { padding: 0 0.8rem; gap: 0.8rem; }
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
/* ββ TABLES ββ
|
| 347 |
+
Nineteen of them are direct children of main with no wrapper. The
|
| 348 |
+
longest unbreakable cell is a weight name like
|
| 349 |
+
transformer.h.N.post_attention_layernorm.weight, which cannot shrink,
|
| 350 |
+
so on a phone the table pushed the BODY sideways rather than scrolling
|
| 351 |
+
inside itself. Each table gets its own scroll container instead. */
|
| 352 |
+
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 1rem 0; }
|
| 353 |
+
table {
|
| 354 |
+
width: 100%;
|
| 355 |
+
border-collapse: collapse;
|
| 356 |
+
font-size: 0.82rem;
|
| 357 |
+
margin: 1rem 0 1.5rem;
|
| 358 |
+
border: 1px solid var(--border);
|
| 359 |
+
border-radius: 8px;
|
| 360 |
+
overflow: hidden;
|
| 361 |
+
}
|
| 362 |
+
th {
|
| 363 |
+
background: var(--surface2);
|
| 364 |
+
padding: 0.65rem 1rem;
|
| 365 |
+
text-align: left;
|
| 366 |
+
font-weight: 600;
|
| 367 |
+
color: var(--text);
|
| 368 |
+
border-bottom: 1px solid var(--border);
|
| 369 |
+
}
|
| 370 |
+
td {
|
| 371 |
+
padding: 0.6rem 1rem;
|
| 372 |
+
color: #b0b0bc;
|
| 373 |
+
border-bottom: 1px solid var(--border);
|
| 374 |
+
vertical-align: middle;
|
| 375 |
+
}
|
| 376 |
+
tr:last-child td { border-bottom: none; }
|
| 377 |
+
tr:hover td { background: #ffffff03; }
|
| 378 |
+
|
| 379 |
+
/* ββ SHADER CARD ββ */
|
| 380 |
+
.shader-doc-card {
|
| 381 |
+
background: var(--surface);
|
| 382 |
+
border: 1px solid var(--border);
|
| 383 |
+
border-radius: 12px;
|
| 384 |
+
padding: 1.25rem 1.5rem;
|
| 385 |
+
margin: 1.25rem 0;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.shader-doc-card h4 {
|
| 389 |
+
font-size: 0.88rem;
|
| 390 |
+
font-weight: 700;
|
| 391 |
+
color: var(--text);
|
| 392 |
+
text-transform: none;
|
| 393 |
+
letter-spacing: 0;
|
| 394 |
+
margin-top: 0;
|
| 395 |
+
display: flex;
|
| 396 |
+
align-items: center;
|
| 397 |
+
gap: 0.65rem;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.shader-doc-card p { font-size: 0.82rem; color: var(--muted); margin: 0.4rem 0 0; }
|
| 401 |
+
|
| 402 |
+
.binding-table { margin: 0.75rem 0 0; }
|
| 403 |
+
.binding-table th, .binding-table td { padding: 0.4rem 0.75rem; font-size: 0.75rem; }
|
| 404 |
+
|
| 405 |
+
/* ββ STEP FLOW ββ */
|
| 406 |
+
.step-flow { display: flex; flex-direction: column; gap: 0; margin: 1rem 0 1.5rem; }
|
| 407 |
+
|
| 408 |
+
.step-row {
|
| 409 |
+
display: flex;
|
| 410 |
+
gap: 1rem;
|
| 411 |
+
align-items: flex-start;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.step-line {
|
| 415 |
+
display: flex;
|
| 416 |
+
flex-direction: column;
|
| 417 |
+
align-items: center;
|
| 418 |
+
flex-shrink: 0;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.step-dot {
|
| 422 |
+
width: 28px; height: 28px;
|
| 423 |
+
border-radius: 50%;
|
| 424 |
+
background: var(--accent);
|
| 425 |
+
color: #fff;
|
| 426 |
+
font-size: 0.68rem;
|
| 427 |
+
font-weight: 700;
|
| 428 |
+
display: flex;
|
| 429 |
+
align-items: center;
|
| 430 |
+
justify-content: center;
|
| 431 |
+
flex-shrink: 0;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.step-line-seg {
|
| 435 |
+
width: 2px;
|
| 436 |
+
flex: 1;
|
| 437 |
+
background: var(--border);
|
| 438 |
+
margin: 4px 0;
|
| 439 |
+
min-height: 20px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.step-content { padding: 0.05rem 0 1.25rem; }
|
| 443 |
+
.step-content h5 { font-size: 0.88rem; font-weight: 700; color: var(--text); margin-bottom: 0.25rem; }
|
| 444 |
+
.step-content p { font-size: 0.8rem; color: var(--muted); margin: 0; }
|
| 445 |
+
|
| 446 |
+
/* ββ DIVIDER ββ */
|
| 447 |
+
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
| 448 |
+
|
| 449 |
+
/* ββ SCROLL MARGIN ββ */
|
| 450 |
+
[id] { scroll-margin-top: calc(var(--topbar-h) + 1.5rem); }
|
| 451 |
+
</style>
|
| 452 |
+
</head>
|
| 453 |
+
<body>
|
| 454 |
+
|
| 455 |
+
<!-- ββ TOPBAR ββ -->
|
| 456 |
+
<div class="topbar">
|
| 457 |
+
<a href="/" class="logo">Zero<span>TVM</span></a>
|
| 458 |
+
<span class="topbar-sep">/</span>
|
| 459 |
+
<span class="topbar-title">Documentation</span>
|
| 460 |
+
<nav class="topbar-nav">
|
| 461 |
+
<a href="/">Home</a>
|
| 462 |
+
<a href="zero-tvm.html" class="topbar-cta">Open Chat β</a>
|
| 463 |
+
</nav>
|
| 464 |
+
</div>
|
| 465 |
+
|
| 466 |
+
<div class="layout">
|
| 467 |
+
|
| 468 |
+
<!-- ββ SIDEBAR ββ -->
|
| 469 |
+
<aside>
|
| 470 |
+
<div class="sidebar-section">
|
| 471 |
+
<span class="sidebar-section-label">Getting Started</span>
|
| 472 |
+
<a href="#intro" class="sidebar-link active">Introduction</a>
|
| 473 |
+
<a href="#quickstart" class="sidebar-link">Quick start</a>
|
| 474 |
+
<a href="#how-it-works" class="sidebar-link">How it works</a>
|
| 475 |
+
</div>
|
| 476 |
+
|
| 477 |
+
<div class="sidebar-section">
|
| 478 |
+
<span class="sidebar-section-label">Architecture</span>
|
| 479 |
+
<a href="#overview" class="sidebar-link">Overview</a>
|
| 480 |
+
<a href="#weight-loader" class="sidebar-link">Weight loader</a>
|
| 481 |
+
<a href="#tokenizer" class="sidebar-link">Tokenizer</a>
|
| 482 |
+
<a href="#kv-cache" class="sidebar-link">KV cache</a>
|
| 483 |
+
<a href="#decode-loop" class="sidebar-link">Decode loop</a>
|
| 484 |
+
<a href="#ping-pong" class="sidebar-link">Ping-pong buffers</a>
|
| 485 |
+
</div>
|
| 486 |
+
|
| 487 |
+
<div class="sidebar-section">
|
| 488 |
+
<span class="sidebar-section-label">WGSL Kernel Roles</span>
|
| 489 |
+
<a href="#shaders" class="sidebar-link">Overview</a>
|
| 490 |
+
<a href="#shader-embedding" class="sidebar-link sub">1 Β· Embedding</a>
|
| 491 |
+
<a href="#shader-rmsnorm" class="sidebar-link sub">2 Β· RMSNorm</a>
|
| 492 |
+
<a href="#shader-qkv" class="sidebar-link sub">3 Β· QKV+RoPE+KV (fused)</a>
|
| 493 |
+
<a href="#shader-attention" class="sidebar-link sub">4 Β· Paged Attention</a>
|
| 494 |
+
<a href="#shader-oproj" class="sidebar-link sub">5 Β· int4 Matmul</a>
|
| 495 |
+
<a href="#shader-ffn" class="sidebar-link sub">6 Β· Fused FFN</a>
|
| 496 |
+
<a href="#shader-addnorm" class="sidebar-link sub">7 Β· Add + RMSNorm</a>
|
| 497 |
+
<a href="#shader-kvappend" class="sidebar-link sub">8 Β· KV Append (prefill)</a>
|
| 498 |
+
<a href="#shader-rope" class="sidebar-link sub">9 Β· RoPE (prefill)</a>
|
| 499 |
+
<a href="#shader-lmhead" class="sidebar-link sub">10 Β· Argmax</a>
|
| 500 |
+
</div>
|
| 501 |
+
|
| 502 |
+
<div class="sidebar-section">
|
| 503 |
+
<span class="sidebar-section-label">Model</span>
|
| 504 |
+
<a href="#phi3" class="sidebar-link">Phi-3 constants</a>
|
| 505 |
+
<a href="#quantization" class="sidebar-link">Q4F16 format</a>
|
| 506 |
+
<a href="#weight-loader" class="sidebar-link">Weight naming</a>
|
| 507 |
+
<a href="#qwen3" class="sidebar-link">Qwen3-4B port</a>
|
| 508 |
+
<a href="#qwen35" class="sidebar-link">Qwen3.5-4B hybrid</a>
|
| 509 |
+
<a href="#qwen36" class="sidebar-link">Qwen3.6 MoE</a>
|
| 510 |
+
</div>
|
| 511 |
+
|
| 512 |
+
<div class="sidebar-section">
|
| 513 |
+
<span class="sidebar-section-label">Extending</span>
|
| 514 |
+
<a href="#porting" class="sidebar-link">Port to Phi-4 / Qwen3</a>
|
| 515 |
+
<a href="#local-weights" class="sidebar-link">Local weight serving</a>
|
| 516 |
+
<a href="#debugging" class="sidebar-link">Debugging tips</a>
|
| 517 |
+
</div>
|
| 518 |
+
|
| 519 |
+
<div class="sidebar-section">
|
| 520 |
+
<span class="sidebar-section-label">Reference</span>
|
| 521 |
+
<a href="#bugs-fixed" class="sidebar-link">Bugs we fixed</a>
|
| 522 |
+
<a href="#vs-webllm" class="sidebar-link">vs WebLLM</a>
|
| 523 |
+
</div>
|
| 524 |
+
</aside>
|
| 525 |
+
|
| 526 |
+
<!-- ββ MAIN ββ -->
|
| 527 |
+
<main>
|
| 528 |
+
|
| 529 |
+
<!-- INTRO -->
|
| 530 |
+
<div id="intro">
|
| 531 |
+
<h1>Zero TVM Documentation</h1>
|
| 532 |
+
<p class="lead">
|
| 533 |
+
A complete LLM inference engine in the browser β <strong>no WebLLM, no TVM, no ONNX, no WASM runtime</strong>.
|
| 534 |
+
10 hand-written WGSL kernel roles, a BPE tokenizer, and raw WebGPU.
|
| 535 |
+
Measured +16.0% on total wall-clock throughput and +31.4% on decode against WebLLM's TVM-autotuned
|
| 536 |
+
kernels, identical weights, same session (M2 Max, 2026-07-30).
|
| 537 |
+
</p>
|
| 538 |
+
|
| 539 |
+
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1.5rem;">
|
| 540 |
+
<span class="pill green">WebGPU</span>
|
| 541 |
+
<span class="pill purple">10 kernel roles</span>
|
| 542 |
+
<span class="pill yellow">Q4F16_1</span>
|
| 543 |
+
<span class="pill green">Phi-3-mini 3.8B</span>
|
| 544 |
+
<span class="pill purple">Paged KV Cache</span>
|
| 545 |
+
<span class="pill green">0 dependencies</span>
|
| 546 |
+
</div>
|
| 547 |
+
|
| 548 |
+
<p>
|
| 549 |
+
This project implements the full Phi-3-mini-4k-instruct transformer forward pass using only
|
| 550 |
+
WebGPU compute shaders written by hand in WGSL. Weights are loaded directly from HuggingFace
|
| 551 |
+
in MLC Q4F16_1 format (cached in OPFS after the first load).
|
| 552 |
+
The tokenizer is implemented in pure TypeScript β no SentencePiece WASM.
|
| 553 |
+
</p>
|
| 554 |
+
|
| 555 |
+
<div class="callout tip">
|
| 556 |
+
<div class="callout-title">Why?</div>
|
| 557 |
+
<p>WebLLM compiles its kernels with TVM; transformers.js runs ONNX Runtime Web. Both work well. Neither is something you read β this is.
|
| 558 |
+
This project exists to show that you can understand <em>every single step</em> of a modern transformer
|
| 559 |
+
inference pipeline, written at the GPU level, in a browser tab.</p>
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
|
| 563 |
+
<!-- QUICKSTART -->
|
| 564 |
+
<h2 id="quickstart">Quick start</h2>
|
| 565 |
+
|
| 566 |
+
<p>No install. Just open the chat page. Weights load from your browser cache (if you've used WebLLM before) or download fresh from HuggingFace (~2 GB).</p>
|
| 567 |
+
|
| 568 |
+
<div class="step-flow">
|
| 569 |
+
<div class="step-row">
|
| 570 |
+
<div class="step-line"><div class="step-dot">1</div><div class="step-line-seg"></div></div>
|
| 571 |
+
<div class="step-content">
|
| 572 |
+
<h5>Open the chat</h5>
|
| 573 |
+
<p>Navigate to <code>zero-tvm.html</code>. WebGPU initializes automatically. Requires WebGPU with shader-f16: Chrome 113+ or Edge 113+. Safari did not enable WebGPU by default until 26, and the writable OPFS API the weight cache needs (createWritable) is missing before then. Subgroups are required for the MoE models.</p>
|
| 574 |
+
</div>
|
| 575 |
+
</div>
|
| 576 |
+
<div class="step-row">
|
| 577 |
+
<div class="step-line"><div class="step-dot">2</div><div class="step-line-seg"></div></div>
|
| 578 |
+
<div class="step-content">
|
| 579 |
+
<h5>Wait for weights</h5>
|
| 580 |
+
<p>First load downloads ~2 GB. Subsequent loads are instant from OPFS. On a small machine, <code>?ctx=</code> shrinks the context window and with it the KV allocation (~1.5 GB at Phi-3βs default 4K). Progress shown per shard in the log panel.</p>
|
| 581 |
+
</div>
|
| 582 |
+
</div>
|
| 583 |
+
<div class="step-row">
|
| 584 |
+
<div class="step-line"><div class="step-dot">3</div><div class="step-line-seg"></div></div>
|
| 585 |
+
<div class="step-content">
|
| 586 |
+
<h5>Chat</h5>
|
| 587 |
+
<p>Phi-3-mini runs at 69.6 tok/s total (83.1 tok/s decode) on M2 Max β +16.0% / +31.4% vs WebLLM on identical weights, same session. Your conversation never leaves the browser. No API calls during inference.</p>
|
| 588 |
+
</div>
|
| 589 |
+
</div>
|
| 590 |
+
<div class="step-row">
|
| 591 |
+
<div class="step-line"><div class="step-dot">4</div></div>
|
| 592 |
+
<div class="step-content">
|
| 593 |
+
<h5>(Optional) Cache weights locally</h5>
|
| 594 |
+
<p>Run <code>node scripts/download-weights.mjs</code> once to save the shards to <code>.weights-local/</code>. Subsequent loads are served from localhost at full disk speed.</p>
|
| 595 |
+
</div>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
|
| 599 |
+
<div class="callout warning">
|
| 600 |
+
<div class="callout-title">GPU requirement</div>
|
| 601 |
+
<p>Requires a GPU with <code>shader-f16</code> WebGPU feature (f16 arithmetic). Most M-series Macs and recent NVIDIA/AMD GPUs support this. Intel integrated graphics may not.</p>
|
| 602 |
+
</div>
|
| 603 |
+
|
| 604 |
+
<!-- HOW IT WORKS -->
|
| 605 |
+
<h2 id="how-it-works">How it works</h2>
|
| 606 |
+
|
| 607 |
+
<p>The engine has three phases per generated token:</p>
|
| 608 |
+
|
| 609 |
+
<ol>
|
| 610 |
+
<li><strong>Prefill</strong> β process the prompt in chunks (per token on specs that cannot chunk), building up the KV cache</li>
|
| 611 |
+
<li><strong>First decode</strong> β the last prefill step produces the first generated token</li>
|
| 612 |
+
<li><strong>Decode loop</strong> β each step takes the previous token as input, runs the full forward pass with the KV cache providing attention context, produces the next token</li>
|
| 613 |
+
</ol>
|
| 614 |
+
|
| 615 |
+
<p>Each forward pass runs <strong>10 kernel roles</strong> through 32 transformer layers β <strong>260 dispatches per token</strong> on the default path β split-K attention has been on by default since 2026-07-27 and adds a combine dispatch per layer; <code>?splitk=0</code> runs the 228-dispatch reference chain β then reads one i32 token ID back from the GPU. For comparison, WebLLM's TVM-generated decode path fires 342 dispatches per token β around 11 distinct shaders on that path, of 85 captured across a whole session.</p>
|
| 616 |
+
|
| 617 |
+
<!-- ARCHITECTURE -->
|
| 618 |
+
<h2 id="overview">Architecture overview</h2>
|
| 619 |
+
|
| 620 |
+
<p>The engineβs main source files:</p>
|
| 621 |
+
|
| 622 |
+
<div class="table-scroll"><table>
|
| 623 |
+
<thead><tr><th>File</th><th>Role</th></tr></thead>
|
| 624 |
+
<tbody>
|
| 625 |
+
<tr><td><code>src/zero-tvm/chat.ts</code></td><td>Main decode engine + UI. Allocates buffers, builds bind groups, runs the decode loop.</td></tr>
|
| 626 |
+
<tr><td><code>src/zero-tvm/weight-loader.ts</code></td><td>Fetches ndarray-cache.json, downloads shards, uploads to GPU buffers.</td></tr>
|
| 627 |
+
<tr><td><code>src/zero-tvm/tokenizer.ts</code></td><td>BPE tokenizer: encode text β token IDs, decode IDs β text.</td></tr>
|
| 628 |
+
<tr><td><code>src/compiler/compiler.ts</code></td><td>Compiles every shader β the hand-written WGSL files plus the generated int4-matmul variants (tiled/subgroup/affine/MoE) β into <code>GPUComputePipeline</code> objects.</td></tr>
|
| 629 |
+
</tbody>
|
| 630 |
+
</table></div>
|
| 631 |
+
|
| 632 |
+
<!-- WEIGHT LOADER -->
|
| 633 |
+
<h2 id="weight-loader">Weight loader</h2>
|
| 634 |
+
|
| 635 |
+
<p>
|
| 636 |
+
Weights are stored in MLC's <strong>ndarray-cache.json</strong> format β an index file listing every parameter,
|
| 637 |
+
which shard binary it lives in, its byte offset, and byte size.
|
| 638 |
+
The loader reads this index then fetches each referenced shard.
|
| 639 |
+
</p>
|
| 640 |
+
|
| 641 |
+
<h3>Fetch priority</h3>
|
| 642 |
+
<ol>
|
| 643 |
+
<li><strong>OPFS</strong> β where this engine caches; a returning visitor loads from here</li>
|
| 644 |
+
<li><strong>Browser Cache API</strong> β read-only leftover from prior WebLLM sessions</li>
|
| 645 |
+
<li><strong>HuggingFace</strong> β direct HTTPS fetch from <code>huggingface.co/mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC</code></li>
|
| 646 |
+
</ol>
|
| 647 |
+
|
| 648 |
+
<div class="callout danger">
|
| 649 |
+
<div class="callout-title">Version mismatch pitfall</div>
|
| 650 |
+
<p>Do not mix a locally-downloaded <code>ndarray-cache.json</code> with shards from the browser cache.
|
| 651 |
+
The byte offsets in the index must match the shards exactly. If you download the index fresh but use
|
| 652 |
+
cached shards from an older model version, all weight slices will be wrong β zero logits β <code><unk></code> output.</p>
|
| 653 |
+
</div>
|
| 654 |
+
|
| 655 |
+
<h3>Parameter naming (MLC format)</h3>
|
| 656 |
+
<p>MLC uses non-standard parameter names. The actual names in the cache:</p>
|
| 657 |
+
|
| 658 |
+
<div class="table-scroll"><table>
|
| 659 |
+
<thead><tr><th>MLC name</th><th>Role</th></tr></thead>
|
| 660 |
+
<tbody>
|
| 661 |
+
<tr><td><code>transformer.embd.q_weight</code></td><td>Embedding weights (uint32 packed int4)</td></tr>
|
| 662 |
+
<tr><td><code>transformer.embd.q_scale</code></td><td>Embedding scales (f16)</td></tr>
|
| 663 |
+
<tr><td><code>transformer.norm.weight</code></td><td>Final RMSNorm gamma (after all layers)</td></tr>
|
| 664 |
+
<tr><td><code>lm_head.q_weight</code></td><td>LM head weights</td></tr>
|
| 665 |
+
<tr><td><code>transformer.h.N.ln.weight</code></td><td>Layer N input_layernorm (normGamma1)</td></tr>
|
| 666 |
+
<tr><td><code>transformer.h.N.post_attention_layernorm.weight</code></td><td>Layer N post-attention norm (normGamma2)</td></tr>
|
| 667 |
+
<tr><td><code>transformer.h.N.mixer.qkv_proj.q_weight</code></td><td>Layer N QKV projection weights</td></tr>
|
| 668 |
+
<tr><td><code>transformer.h.N.mixer.out_proj.q_weight</code></td><td>Layer N output projection weights</td></tr>
|
| 669 |
+
<tr><td><code>transformer.h.N.mlp.gate_up_proj.q_weight</code></td><td>Layer N FFN gate+up weights</td></tr>
|
| 670 |
+
<tr><td><code>transformer.h.N.mlp.down_proj.q_weight</code></td><td>Layer N FFN down weights</td></tr>
|
| 671 |
+
</tbody>
|
| 672 |
+
</table></div>
|
| 673 |
+
|
| 674 |
+
<!-- TOKENIZER -->
|
| 675 |
+
<h2 id="tokenizer">Tokenizer</h2>
|
| 676 |
+
|
| 677 |
+
<p>
|
| 678 |
+
A hand-written BPE tokenizer in TypeScript. No SentencePiece WASM, no HuggingFace tokenizers bundle.
|
| 679 |
+
Reads <code>tokenizer.json</code> directly.
|
| 680 |
+
</p>
|
| 681 |
+
|
| 682 |
+
<h3>Key steps</h3>
|
| 683 |
+
<ul>
|
| 684 |
+
<li><strong>Pre-tokenization</strong> β Metaspace: spaces become <code>β</code>, words are split on whitespace</li>
|
| 685 |
+
<li><strong>BPE encoding</strong> β merge pairs by rank from the merge table in tokenizer.json</li>
|
| 686 |
+
<li><strong>Special tokens</strong> β <code><|system|></code>, <code><|user|></code>, <code><|assistant|></code>, <code><|end|></code></li>
|
| 687 |
+
<li><strong>Chat template</strong> β Phi-3 format applied by <code>buildChatPrompt()</code></li>
|
| 688 |
+
</ul>
|
| 689 |
+
|
| 690 |
+
<h3>Phi-3 chat template</h3>
|
| 691 |
+
<div class="code-block">
|
| 692 |
+
<div class="code-label">π prompt format</div>
|
| 693 |
+
<pre><code><|system|>
|
| 694 |
+
You are a helpful assistant.<|end|>
|
| 695 |
+
<|user|>
|
| 696 |
+
What is the capital of Australia?<|end|>
|
| 697 |
+
<|assistant|></code></pre>
|
| 698 |
+
</div>
|
| 699 |
+
|
| 700 |
+
<p>Stop tokens: <code>2</code> (EOS), <code>32000</code> (<code><|end|></code>), <code>32007</code> (<code><|endoftext|></code>).</p>
|
| 701 |
+
|
| 702 |
+
<!-- KV CACHE -->
|
| 703 |
+
<h2 id="kv-cache">KV cache</h2>
|
| 704 |
+
|
| 705 |
+
<p>
|
| 706 |
+
Uses a <strong>paged KV cache</strong> layout. Memory is divided into fixed-size pages (16 slots each)
|
| 707 |
+
and a page table maps logical positions to physical pages. This is <em>not</em> vLLM-style relocatable
|
| 708 |
+
blocks: K is RoPE’d before the cache write on every path, so a cached page is valid only at the
|
| 709 |
+
positions it was written at. It is a prefix pool, and cannot become a block pool without moving RoPE.
|
| 710 |
+
</p>
|
| 711 |
+
|
| 712 |
+
<div class="table-scroll"><table>
|
| 713 |
+
<thead><tr><th>Parameter</th><th>Value</th><th>Notes</th></tr></thead>
|
| 714 |
+
<tbody>
|
| 715 |
+
<tr><td><code>PAGE_SIZE</code></td><td>16</td><td>slots per page</td></tr>
|
| 716 |
+
<tr><td><code>MAX_PAGES</code></td><td>257</td><td>β 4096 context tokens</td></tr>
|
| 717 |
+
<tr><td>Bytes per page</td><td>196,608</td><td>32 heads Γ 16 slots Γ 96 dims Γ 2 (K+V) Γ 2 bytes</td></tr>
|
| 718 |
+
<tr><td>Total KV buffer</td><td>~50 MB per layer</td><td>32 layers = ~1.6 GB</td></tr>
|
| 719 |
+
</tbody>
|
| 720 |
+
</table></div>
|
| 721 |
+
|
| 722 |
+
<p>Each layer has its own <code>GPUBuffer</code> for KV pages. The page table is a simple identity mapping (page i β physical page i) for single-sequence inference.</p>
|
| 723 |
+
|
| 724 |
+
<!-- DECODE LOOP -->
|
| 725 |
+
<h2 id="decode-loop">Decode loop</h2>
|
| 726 |
+
|
| 727 |
+
<p>Each call to <code>decodeToken(tokenId, position)</code> submits one command encoder with the full forward pass:</p>
|
| 728 |
+
|
| 729 |
+
<div class="code-block">
|
| 730 |
+
<div class="code-label">src/zero-tvm/chat.ts Β· decodeToken()</div>
|
| 731 |
+
<pre><code><span class="cm">// Per-token GPU state written via writeBuffer</span>
|
| 732 |
+
B.inputIds β [tokenId] <span class="cm">// i32</span>
|
| 733 |
+
B.posMap β [position] <span class="cm">// i32</span>
|
| 734 |
+
B.pageIndptr β [0, nnzPages] <span class="cm">// page range</span>
|
| 735 |
+
B.lengthInfo β [position+1, 0, 0] <span class="cm">// seq length</span>
|
| 736 |
+
|
| 737 |
+
<span class="cm">// Forward pass (one command encoder)</span>
|
| 738 |
+
embedding(B.residual) <span class="cm">// token β hidden state</span>
|
| 739 |
+
rmsNorm(B.hidden1, B.residual) <span class="cm">// initial norm</span>
|
| 740 |
+
|
| 741 |
+
<span class="kw">for</span> L <span class="op">in</span> <span class="num">0</span>..<span class="num">32</span>:
|
| 742 |
+
<span class="cm">// QKV matmul + RoPE + KV-append in ONE dispatch (M4 fusion)</span>
|
| 743 |
+
qkvFused(B.qOut, kvPages[L], B.hidden1)
|
| 744 |
+
attention(B.attnOut, B.qOut, kvPages[L])
|
| 745 |
+
int4Matmul(B.hidden2, B.attnOut) <span class="cm">// O projection</span>
|
| 746 |
+
addNorm(B.hidden2, resIn β B.hidden1, resOut) <span class="cm">// residual + RMSNorm, ping-pong</span>
|
| 747 |
+
<span class="cm">// Gate + Up + SiLU + mul + Down in ONE dispatch</span>
|
| 748 |
+
fusedFfn(B.hidden2, B.hidden1)
|
| 749 |
+
addNorm(B.hidden2, resIn β B.hidden1, resOut) <span class="cm">// residual + RMSNorm, ping-pong</span>
|
| 750 |
+
|
| 751 |
+
int4Matmul(B.logits, B.hidden1) <span class="cm">// LM head</span>
|
| 752 |
+
argmax(B.tokenOut, B.logits) <span class="cm">// β next token ID</span></code></pre>
|
| 753 |
+
</div>
|
| 754 |
+
|
| 755 |
+
<!-- PING-PONG -->
|
| 756 |
+
<h2 id="ping-pong">Ping-pong residual buffers</h2>
|
| 757 |
+
|
| 758 |
+
<p>
|
| 759 |
+
WebGPU's validation rules forbid binding the same buffer as both <code>read</code> and <code>read_write</code>
|
| 760 |
+
in the same dispatch. The add_norm shader needs to <em>read</em> the old residual and <em>write</em> the new one.
|
| 761 |
+
</p>
|
| 762 |
+
|
| 763 |
+
<p>Solution: two residual buffers that alternate each dispatch.</p>
|
| 764 |
+
|
| 765 |
+
<div class="code-block">
|
| 766 |
+
<div class="code-label">ping-pong pattern</div>
|
| 767 |
+
<pre><code><span class="kw">let</span> resIn = B.residual <span class="cm">// ping (starts with embedding)</span>
|
| 768 |
+
<span class="kw">let</span> resOut = B.residual2 <span class="cm">// pong (uninitialized)</span>
|
| 769 |
+
|
| 770 |
+
<span class="cm">// Each add_norm:</span>
|
| 771 |
+
dispatch(addNorm, [delta, resIn, gamma, hidden1, resOut])
|
| 772 |
+
[resIn, resOut] = [resOut, resIn] <span class="cm">// swap β O(1), no GPU copy</span></code></pre>
|
| 773 |
+
</div>
|
| 774 |
+
|
| 775 |
+
<div class="callout info">
|
| 776 |
+
<p>The swap is just two JavaScript variable reassignments β no GPU buffer copy. Both buffers always exist on the GPU; we just change which one we tell the bind group to read vs write.</p>
|
| 777 |
+
</div>
|
| 778 |
+
|
| 779 |
+
<!-- SHADERS -->
|
| 780 |
+
<h2 id="shaders">WGSL Kernel Roles</h2>
|
| 781 |
+
|
| 782 |
+
<p>All shaders live in <code>src/compiler/shaders/</code>, implementing 10 distinct kernel roles. The rest are tiled and subgroup variants of the same role, selectable at runtime via URL flags. The compiler compiles them all at startup into <code>GPUComputePipeline</code> objects.</p>
|
| 783 |
+
|
| 784 |
+
<p>Binding convention: <code>@group(0)</code> always. Binding indices are <strong>zero-based</strong> and match the order you pass buffers to <code>bg(device, pipeline, [...bufs])</code>.</p>
|
| 785 |
+
|
| 786 |
+
<div class="callout tip">
|
| 787 |
+
<div class="callout-title">Decode vs prefill paths diverge</div>
|
| 788 |
+
<p>The decode loop uses the fused <code>qkv_fused</code> kernel (QKV matmul + RoPE + KV-append all in one dispatch). Prefill still uses separate <code>int4_matmul</code> + <code>rope</code> + <code>kv_append</code> dispatches because prefill processes many tokens at once, and the fusion win only lands for <code>ntoken=1</code>.</p>
|
| 789 |
+
</div>
|
| 790 |
+
|
| 791 |
+
<hr>
|
| 792 |
+
|
| 793 |
+
<!-- SHADER 1 -->
|
| 794 |
+
<div id="shader-embedding">
|
| 795 |
+
<div class="shader-doc-card">
|
| 796 |
+
<h4>π€ 1 Β· Embedding <span class="pill purple">embedding.wgsl</span></h4>
|
| 797 |
+
<p>Token ID lookup with Q4F16 dequantization. Each output element is dequantized from a packed int4 value: <code>(nibble - 7) Γ scale</code>.</p>
|
| 798 |
+
<div class="table-scroll"><table class="binding-table">
|
| 799 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 800 |
+
<tbody>
|
| 801 |
+
<tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>output hidden state</td></tr>
|
| 802 |
+
<tr><td><code>@1</code></td><td><code>read i32[]</code></td><td>input token IDs</td></tr>
|
| 803 |
+
<tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>scales (group_size=32)</td></tr>
|
| 804 |
+
<tr><td><code>@3</code></td><td><code>read u32[]</code></td><td>packed weights (8 int4 per u32)</td></tr>
|
| 805 |
+
<tr><td><code>@4</code></td><td><code>uniform</code></td><td><code>{ seq_len, packGridDimX }</code></td></tr>
|
| 806 |
+
</tbody>
|
| 807 |
+
</table></div>
|
| 808 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 12 workgroups Γ 256 threads = 3072 output elements (D=3072)</p>
|
| 809 |
+
</div>
|
| 810 |
+
</div>
|
| 811 |
+
|
| 812 |
+
<!-- SHADER 2 -->
|
| 813 |
+
<div id="shader-rmsnorm">
|
| 814 |
+
<div class="shader-doc-card">
|
| 815 |
+
<h4>π 2 Β· RMSNorm <span class="pill purple">rms_norm.wgsl</span></h4>
|
| 816 |
+
<p>Root mean square layer normalization. Computes <code>x / sqrt(mean(xΒ²) + Ξ΅) Γ gamma</code>. Uses 256-thread tree reduction in workgroup shared memory.</p>
|
| 817 |
+
<div class="table-scroll"><table class="binding-table">
|
| 818 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 819 |
+
<tbody>
|
| 820 |
+
<tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>normalized output</td></tr>
|
| 821 |
+
<tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>input</td></tr>
|
| 822 |
+
<tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>gamma weights</td></tr>
|
| 823 |
+
<tr><td><code>@3</code></td><td><code>uniform</code></td><td><code>{ packGridDimX }</code></td></tr>
|
| 824 |
+
</tbody>
|
| 825 |
+
</table></div>
|
| 826 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 workgroup (one token, D=3072)</p>
|
| 827 |
+
</div>
|
| 828 |
+
</div>
|
| 829 |
+
|
| 830 |
+
<!-- SHADER 3 Β· QKV FUSED (decode) -->
|
| 831 |
+
<div id="shader-qkv">
|
| 832 |
+
<div class="shader-doc-card">
|
| 833 |
+
<h4>β‘ 3 Β· QKV + RoPE + KV-append (fused, decode) <span class="pill purple">qkv_fused.wgsl</span></h4>
|
| 834 |
+
<p>
|
| 835 |
+
The big M4 fusion. One dispatch replaces three on the decode path: the int4 QKV matmul, the RoPE rotation of Q and K, and the write of K/V into the paged KV cache. Each workgroup computes two output rows that form a RoPE pair (dim and dim+48 within the same head), rotates the pair in registers, and writes K/V straight into <code>kv_pages</code> β the intermediate <code>qkv</code> / <code>k_out</code> / <code>v_out</code> buffers from the pre-fusion path are skipped entirely.
|
| 836 |
+
</p>
|
| 837 |
+
<div class="table-scroll"><table class="binding-table">
|
| 838 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 839 |
+
<tbody>
|
| 840 |
+
<tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>q_out [3072]</td></tr>
|
| 841 |
+
<tr><td><code>@1</code></td><td><code>read_write f16[]</code></td><td>kv_pages (paged KV cache)</td></tr>
|
| 842 |
+
<tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>hidden [3072]</td></tr>
|
| 843 |
+
<tr><td><code>@3</code></td><td><code>read f16[]</code></td><td>scales [9216 Γ 96]</td></tr>
|
| 844 |
+
<tr><td><code>@4</code></td><td><code>read u32[]</code></td><td>packed weights [9216 Γ 384]</td></tr>
|
| 845 |
+
<tr><td><code>@5</code></td><td><code>read i32[]</code></td><td>position map</td></tr>
|
| 846 |
+
<tr><td><code>@6</code></td><td><code>uniform</code></td><td><code>{ position_map_elem_offset, pages_elem_offset, packGridDimX }</code></td></tr>
|
| 847 |
+
</tbody>
|
| 848 |
+
</table></div>
|
| 849 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 4,608 workgroups (down from 9,216 matmul + 36 RoPE + 12 KV-append = 9,264 in the pre-fusion path). Decode-only; prefill still uses the 3-dispatch path (see shaders 8 and 9).</p>
|
| 850 |
+
</div>
|
| 851 |
+
</div>
|
| 852 |
+
|
| 853 |
+
<!-- SHADER 4 Β· PAGED ATTENTION -->
|
| 854 |
+
<div id="shader-attention">
|
| 855 |
+
<div class="shader-doc-card">
|
| 856 |
+
<h4>ποΈ 4 Β· Paged Attention <span class="pill purple">attention.wgsl</span></h4>
|
| 857 |
+
<p>Multi-head attention over the paged KV cache. Reads K and V from pages, computes scaled dot-product attention with an online-softmax reduction in shared memory. Each workgroup handles one attention head.</p>
|
| 858 |
+
<div class="table-scroll"><table class="binding-table">
|
| 859 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 860 |
+
<tbody>
|
| 861 |
+
<tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>Q [3072]</td></tr>
|
| 862 |
+
<tr><td><code>@1</code></td><td><code>read i32[]</code></td><td>page indptr</td></tr>
|
| 863 |
+
<tr><td><code>@2</code></td><td><code>read i32[]</code></td><td>page values (page table)</td></tr>
|
| 864 |
+
<tr><td><code>@3</code></td><td><code>read f16[]</code></td><td>KV pages</td></tr>
|
| 865 |
+
<tr><td><code>@4</code></td><td><code>read i32[]</code></td><td>length info</td></tr>
|
| 866 |
+
<tr><td><code>@5</code></td><td><code>read_write f16[]</code></td><td>attn output [3072]</td></tr>
|
| 867 |
+
<tr><td><code>@6</code></td><td><code>uniform</code></td><td>attention config (scale, pages)</td></tr>
|
| 868 |
+
</tbody>
|
| 869 |
+
</table></div>
|
| 870 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 Γ HEADS workgroups (1 Γ 32). An <code>attention_int8.wgsl</code> variant reads an int8-quantized KV cache; enable via <code>?kv8=1</code>.</p>
|
| 871 |
+
</div>
|
| 872 |
+
</div>
|
| 873 |
+
|
| 874 |
+
<!-- SHADER 5 Β· INT4 MATMUL -->
|
| 875 |
+
<div id="shader-oproj">
|
| 876 |
+
<div class="shader-doc-card">
|
| 877 |
+
<h4>βοΈ 5 Β· int4 Matmul (output projection + LM head) <span class="pill purple">int4_matmul.wgsl</span></h4>
|
| 878 |
+
<p>General-purpose dequantize-on-the-fly int4 Γ f16 matmul. Used for the attention <em>output projection</em> (3072 β 3072) and the <em>LM head</em> (3072 β 32064). Weights are Q4F16_1: <code>N</code> output rows Γ 384 u32 columns (each u32 = 8 int4 values = 32 elements, group_size=32). The tiled / subgroup / vec4 variants are emitted by one generator beside it (<code>int4_matmul.gen.ts</code>); the runtime picks one via the <code>?matmul=</code> URL flag.</p>
|
| 879 |
+
<div class="table-scroll"><table class="binding-table">
|
| 880 |
+
<thead><tr><th>Uniform field</th><th>Value</th></tr></thead>
|
| 881 |
+
<tbody>
|
| 882 |
+
<tr><td><code>K_groups</code></td><td>384 (= input_dim / 8)</td></tr>
|
| 883 |
+
<tr><td><code>scale_stride</code></td><td>96 (= input_dim / group_size)</td></tr>
|
| 884 |
+
<tr><td><code>N</code></td><td>3072 (o-proj) or 32064 (lm_head)</td></tr>
|
| 885 |
+
</tbody>
|
| 886 |
+
</table></div>
|
| 887 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> <code>N</code> workgroups β 3,072 for o-proj, 32,064 for lm_head.</p>
|
| 888 |
+
</div>
|
| 889 |
+
</div>
|
| 890 |
+
|
| 891 |
+
<!-- SHADER 6 Β· FUSED FFN -->
|
| 892 |
+
<div id="shader-ffn">
|
| 893 |
+
<div class="shader-doc-card">
|
| 894 |
+
<h4>π 6 Β· Fused FFN (Gate Β· Up Β· SiLU Β· Mul Β· Down) <span class="pill purple">fused_ffn.wgsl</span></h4>
|
| 895 |
+
<p>
|
| 896 |
+
Gate and up projections (both int4 matmuls sharing the 16,384-row <code>gate_up_proj</code> weight matrix), the SiLU activation and the elementwise multiply, in one dispatch. The down projection back to 3,072 dims is its own <code>int4_matmul</code> dispatch β which is why a layer costs 7 dispatches, not 6.
|
| 897 |
+
</p>
|
| 898 |
+
<div class="table-scroll"><table class="binding-table">
|
| 899 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 900 |
+
<tbody>
|
| 901 |
+
<tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>output [3072]</td></tr>
|
| 902 |
+
<tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>input [3072]</td></tr>
|
| 903 |
+
<tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>gate_up scales</td></tr>
|
| 904 |
+
<tr><td><code>@3</code></td><td><code>read u32[]</code></td><td>gate_up packed weights (16,384 Γ 384)</td></tr>
|
| 905 |
+
<tr><td><code>@4</code></td><td><code>read f16[]</code></td><td>down_proj scales</td></tr>
|
| 906 |
+
<tr><td><code>@5</code></td><td><code>read u32[]</code></td><td>down_proj packed weights (3,072 Γ 1,024)</td></tr>
|
| 907 |
+
<tr><td><code>@6</code></td><td><code>uniform</code></td><td>FFN config</td></tr>
|
| 908 |
+
</tbody>
|
| 909 |
+
</table></div>
|
| 910 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 3,072 workgroups β one per output row of the down projection. A <code>fused_ffn_tiled_sg.wgsl</code> variant uses subgroup reductions; selectable via URL flag.</p>
|
| 911 |
+
</div>
|
| 912 |
+
</div>
|
| 913 |
+
|
| 914 |
+
<!-- SHADER 7 Β· ADD + RMSNORM -->
|
| 915 |
+
<div id="shader-addnorm">
|
| 916 |
+
<div class="shader-doc-card">
|
| 917 |
+
<h4>β 7 Β· Fused Add + RMSNorm <span class="pill purple">add_norm.wgsl</span></h4>
|
| 918 |
+
<p>Residual add + RMSNorm in one pass. Computes <code>residual_out = A + B</code>, then <code>output = RMSNorm(residual_out) Γ gamma</code>. Used twice per layer (post-attention and post-FFN). Mirrors TVM's <code>fuse_add_norm_decode</code>.</p>
|
| 919 |
+
<div class="table-scroll"><table class="binding-table">
|
| 920 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 921 |
+
<tbody>
|
| 922 |
+
<tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>A β the new contribution (O-proj or FFN-down output)</td></tr>
|
| 923 |
+
<tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>B β the running residual (resIn)</td></tr>
|
| 924 |
+
<tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>gamma β normalization weights</td></tr>
|
| 925 |
+
<tr><td><code>@3</code></td><td><code>read_write f16[]</code></td><td>normalized output (B.hidden1)</td></tr>
|
| 926 |
+
<tr><td><code>@4</code></td><td><code>read_write f16[]</code></td><td>new residual (resOut β ping-pong)</td></tr>
|
| 927 |
+
<tr><td><code>@5</code></td><td><code>uniform</code></td><td><code>{ packGridDimX }</code></td></tr>
|
| 928 |
+
</tbody>
|
| 929 |
+
</table></div>
|
| 930 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 workgroup Β· 256 threads Β· 12 elements each = 3,072.</p>
|
| 931 |
+
</div>
|
| 932 |
+
</div>
|
| 933 |
+
|
| 934 |
+
<!-- SHADER 8 Β· KV APPEND (prefill) -->
|
| 935 |
+
<div id="shader-kvappend">
|
| 936 |
+
<div class="shader-doc-card">
|
| 937 |
+
<h4>πΎ 8 Β· KV Append (prefill path) <span class="pill purple">kv_append.wgsl</span></h4>
|
| 938 |
+
<p>Writes K and V vectors into the paged KV cache at the correct slot for each position. On the decode path this work is folded into <code>qkv_fused</code>; on prefill it runs as a separate dispatch because prefill processes many tokens at once and the per-token fusion no longer pays off.</p>
|
| 939 |
+
<div class="table-scroll"><table class="binding-table">
|
| 940 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 941 |
+
<tbody>
|
| 942 |
+
<tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>k_out [3072]</td></tr>
|
| 943 |
+
<tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>v_out [3072]</td></tr>
|
| 944 |
+
<tr><td><code>@2</code></td><td><code>read_write f16[]</code></td><td>KV pages buffer</td></tr>
|
| 945 |
+
<tr><td><code>@3</code></td><td><code>read i32[]</code></td><td>position map</td></tr>
|
| 946 |
+
<tr><td><code>@4</code></td><td><code>uniform</code></td><td>page config</td></tr>
|
| 947 |
+
</tbody>
|
| 948 |
+
</table></div>
|
| 949 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 12 workgroups per token (HEADS=32, HEAD_DIM=96).</p>
|
| 950 |
+
</div>
|
| 951 |
+
</div>
|
| 952 |
+
|
| 953 |
+
<!-- SHADER 9 Β· ROPE (prefill) -->
|
| 954 |
+
<div id="shader-rope">
|
| 955 |
+
<div class="shader-doc-card">
|
| 956 |
+
<h4>π 9 Β· RoPE (prefill path) <span class="pill purple">rope.wgsl</span></h4>
|
| 957 |
+
<p>Rotary position embeddings applied to Q and K. Prefill-only β the decode path folds RoPE into <code>qkv_fused</code>. Splits the concatenated 9,216-dim QKV buffer into Q / K / V, rotates Q and K in place based on position, and copies V unchanged.</p>
|
| 958 |
+
<div class="table-scroll"><table class="binding-table">
|
| 959 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 960 |
+
<tbody>
|
| 961 |
+
<tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>q_out [3072]</td></tr>
|
| 962 |
+
<tr><td><code>@1</code></td><td><code>read_write f16[]</code></td><td>k_out [3072]</td></tr>
|
| 963 |
+
<tr><td><code>@2</code></td><td><code>read_write f16[]</code></td><td>v_out [3072]</td></tr>
|
| 964 |
+
<tr><td><code>@3</code></td><td><code>read f16[]</code></td><td>qkv input [9216]</td></tr>
|
| 965 |
+
<tr><td><code>@4</code></td><td><code>read i32[]</code></td><td>position map</td></tr>
|
| 966 |
+
<tr><td><code>@5</code></td><td><code>uniform</code></td><td>RoPE config</td></tr>
|
| 967 |
+
</tbody>
|
| 968 |
+
</table></div>
|
| 969 |
+
<div class="callout danger" style="margin-top:0.75rem;">
|
| 970 |
+
<div class="callout-title">Critical binding order</div>
|
| 971 |
+
<p>The binding order <strong>must</strong> be <code>[q_out, k_out, v_out, qkv, posMap, uniform]</code>. Swapping these caused a garbage-output bug during development.</p>
|
| 972 |
+
</div>
|
| 973 |
+
<p><strong>Dispatch:</strong> 36 workgroups Γ 256 threads = 9,216 = 3 Γ 3,072.</p>
|
| 974 |
+
</div>
|
| 975 |
+
</div>
|
| 976 |
+
|
| 977 |
+
<!-- SHADER 10 Β· ARGMAX -->
|
| 978 |
+
<div id="shader-lmhead">
|
| 979 |
+
<div class="shader-doc-card">
|
| 980 |
+
<h4>π― 10 Β· Argmax Sampler <span class="pill purple">argmax.wgsl</span></h4>
|
| 981 |
+
<p>
|
| 982 |
+
Parallel-reduction argmax over the 32,064-entry logit buffer produced by the LM-head <code>int4_matmul</code>. Replaces TVM's ~20-dispatch sampling chain (penalty β softmax β cumsum β argsort β gather β β¦) with a single dispatch. Greedy decoding only; top-k / top-p not wired up yet.
|
| 983 |
+
</p>
|
| 984 |
+
<div class="table-scroll"><table class="binding-table">
|
| 985 |
+
<thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
|
| 986 |
+
<tbody>
|
| 987 |
+
<tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>logits [32064]</td></tr>
|
| 988 |
+
<tr><td><code>@1</code></td><td><code>read_write i32[]</code></td><td>output token id [1]</td></tr>
|
| 989 |
+
</tbody>
|
| 990 |
+
</table></div>
|
| 991 |
+
<p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 workgroup (tree reduction over 32,064 logits). An <code>argmax_sg.wgsl</code> subgroup variant is available.</p>
|
| 992 |
+
</div>
|
| 993 |
+
</div>
|
| 994 |
+
|
| 995 |
+
<!-- PHI-3 CONSTANTS -->
|
| 996 |
+
<h2 id="phi3">Phi-3 model constants</h2>
|
| 997 |
+
|
| 998 |
+
<div class="code-block">
|
| 999 |
+
<div class="code-label">src/compiler/compiler.ts</div>
|
| 1000 |
+
<pre><code><span class="kw">export const</span> PHI3 = {
|
| 1001 |
+
D: <span class="num">3072</span>, <span class="cm">// hidden dimension</span>
|
| 1002 |
+
HEADS: <span class="num">32</span>, <span class="cm">// attention heads</span>
|
| 1003 |
+
HEAD_DIM: <span class="num">96</span>, <span class="cm">// D / HEADS</span>
|
| 1004 |
+
LAYERS: <span class="num">32</span>, <span class="cm">// transformer layers</span>
|
| 1005 |
+
FFN: <span class="num">8192</span>, <span class="cm">// FFN intermediate dimension</span>
|
| 1006 |
+
VOCAB: <span class="num">32064</span>, <span class="cm">// vocabulary size</span>
|
| 1007 |
+
PAGE_SIZE:<span class="num">16</span>, <span class="cm">// KV cache slots per page</span>
|
| 1008 |
+
MAX_PAGES:<span class="num">257</span>, <span class="cm">// max pages (β 4096 context)</span>
|
| 1009 |
+
}</code></pre>
|
| 1010 |
+
</div>
|
| 1011 |
+
|
| 1012 |
+
<!-- Q4F16 -->
|
| 1013 |
+
<h2 id="quantization">Q4F16 quantization format</h2>
|
| 1014 |
+
|
| 1015 |
+
<p>
|
| 1016 |
+
MLC's Q4F16_1 format packs 8 int4 values into each <code>uint32</code>.
|
| 1017 |
+
Scales are stored as <code>float16</code> with <code>group_size=32</code> β one scale per 32 weights.
|
| 1018 |
+
</p>
|
| 1019 |
+
|
| 1020 |
+
<div class="code-block">
|
| 1021 |
+
<div class="code-label">Dequantization formula (from embedding.wgsl)</div>
|
| 1022 |
+
<pre><code><span class="cm">// Extract nibble for element i within a u32</span>
|
| 1023 |
+
<span class="kw">let</span> nibble = (packed_u32 >> (i * <span class="num">4</span>)) & <span class="num">0xF</span>;
|
| 1024 |
+
|
| 1025 |
+
<span class="cm">// Dequantize: center around 0, multiply by scale</span>
|
| 1026 |
+
<span class="kw">let</span> value = <span class="ty">f16</span>(i32(nibble) - <span class="num">7</span>) * scale;</code></pre>
|
| 1027 |
+
</div>
|
| 1028 |
+
|
| 1029 |
+
<p>Weight shapes in Q4F16 (for Phi-3-mini):</p>
|
| 1030 |
+
|
| 1031 |
+
<div class="table-scroll"><table>
|
| 1032 |
+
<thead><tr><th>Parameter</th><th>q_weight shape (u32)</th><th>q_scale shape (f16)</th></tr></thead>
|
| 1033 |
+
<tbody>
|
| 1034 |
+
<tr><td>Embedding</td><td>[32064, 384]</td><td>[32064, 96]</td></tr>
|
| 1035 |
+
<tr><td>QKV proj (per layer)</td><td>[9216, 384]</td><td>[9216, 96]</td></tr>
|
| 1036 |
+
<tr><td>O proj (per layer)</td><td>[3072, 384]</td><td>[3072, 96]</td></tr>
|
| 1037 |
+
<tr><td>Gate+Up FFN (per layer)</td><td>[16384, 384]</td><td>[16384, 96]</td></tr>
|
| 1038 |
+
<tr><td>Down FFN (per layer)</td><td>[3072, 1024]</td><td>[3072, 256]</td></tr>
|
| 1039 |
+
<tr><td>LM head</td><td>[32064, 384]</td><td>[32064, 96]</td></tr>
|
| 1040 |
+
</tbody>
|
| 1041 |
+
</table></div>
|
| 1042 |
+
|
| 1043 |
+
<!-- QWEN3 -->
|
| 1044 |
+
<h2 id="qwen3">Qwen3-4B (<code>?model=qwen3</code>)</h2>
|
| 1045 |
+
|
| 1046 |
+
<p>
|
| 1047 |
+
The engine is parameterized over a <code>ModelSpec</code>, and a v1 Qwen3-4B (q4f16_1) port ships alongside Phi-3.
|
| 1048 |
+
Append <code>?model=qwen3</code> to <code>zero-tvm.html</code> or <code>validate.html</code> β it works on the live
|
| 1049 |
+
site (weights stream from HuggingFace, ~2.3 GB), and <code>node scripts/download-weights.mjs --model qwen3</code>
|
| 1050 |
+
primes the local dev mirror. Phi-3 stays the default; all existing URLs keep their exact behavior.
|
| 1051 |
+
</p>
|
| 1052 |
+
|
| 1053 |
+
<p>What the port exercises that Phi-3 doesn't:</p>
|
| 1054 |
+
|
| 1055 |
+
<div class="table-scroll"><table>
|
| 1056 |
+
<thead><tr><th></th><th>Phi-3-mini (default)</th><th>Qwen3-4B</th></tr></thead>
|
| 1057 |
+
<tbody>
|
| 1058 |
+
<tr><td>Attention</td><td>MHA, 32/32 heads</td><td>GQA, 32 query heads over 8 KV heads</td></tr>
|
| 1059 |
+
<tr><td>QK-norm</td><td>none</td><td>per-head RMSNorm on Q and K between projection and RoPE</td></tr>
|
| 1060 |
+
<tr><td>Tokenizer</td><td>SentencePiece</td><td>byte-level BPE (Qwen2-style <code>tokenizer.json</code>)</td></tr>
|
| 1061 |
+
<tr><td>LM head</td><td>separate</td><td>tied β logits reuse the quantized embedding matrix (151,936 vocab)</td></tr>
|
| 1062 |
+
<tr><td>Decode path</td><td>fused, 7 dispatches/layer</td><td>unfused QKV + fused qk_norm+RoPE+append, 8 dispatches/layer</td></tr>
|
| 1063 |
+
</tbody>
|
| 1064 |
+
</table></div>
|
| 1065 |
+
|
| 1066 |
+
<p>
|
| 1067 |
+
Measured 2026-07-30 on an Apple M2 Max under the corrected protocol (same session, identical local weight
|
| 1068 |
+
bytes, both engines paying a full prefill on every run): Zero-TVM <strong>59.85 tok/s total</strong>
|
| 1069 |
+
(TTFT 453 ms, decode 75.49) vs WebLLM 0.2.84's prebuilt Qwen3-4B at <strong>45.46 tok/s total</strong>
|
| 1070 |
+
(self-reported decode 47.77) β <strong>+31.7% total, +58.0% decode</strong>. Unlike the Phi-3 headline,
|
| 1071 |
+
these figures are static dated text, not synced from <code>bench/results.json</code>; the machine-readable
|
| 1072 |
+
record is <code>bench/results/qwen3-4b.json</code>.
|
| 1073 |
+
</p>
|
| 1074 |
+
|
| 1075 |
+
<div class="callout warning">
|
| 1076 |
+
<div class="callout-title">Read it cautiously β one pair, one machine, and the previous number was withdrawn</div>
|
| 1077 |
+
<p>The <strong>"75.74 vs 43.75, +73%" pair published on 2026-07-29 is withdrawn.</strong> It was measured after cross-turn prefix reuse shipped (PR #24) but before the bench harness was fixed to reset it, so the Zero-TVM half prefilled a single token per run while the WebLLM half prefilled the whole prompt β not like-for-like. Full writeup at the top of BENCH.md. The engine work itself is unchanged and its Zero-TVM-vs-Zero-TVM A/Bs still stand: QK-norm must run between the QKV matmul and RoPE, which is incompatible with the fused QKV+RoPE+KV-append kernel β the QKV matmul stays a separate dispatch β but since the 2026-07-29 tuning round everything after it is fused (<code>qk_norm_rope_append</code>: per-head norm + RoPE + paged KV write in one pass, 8 dispatches/layer; <code>?fuseqk=0</code> restores the 10-dispatch reference chain) and the K%512 <code>_vec4h</code> matmul variants give d=2560 / ffn=9728 wide loads (<code>?vec4h=0</code> opts out). Same-day A/Bs: fused-qk +2.3%, vec4h +5.7%, combined +5.8% over the flags-off half. The earlier 2026-07-28 pair (25.43 vs 14.15) did not reproduce on the same machine β both engines moved ~3Γ together (degraded session; control-run details in BENCH.md's tuning-round session note). Qwen3-4B is also the model where WebLLM most clearly beats us on time-to-first-token: its 263β271 tok/s prefill implies ~150 ms against our 453 ms.</p>
|
| 1078 |
+
</div>
|
| 1079 |
+
|
| 1080 |
+
<!-- QWEN35 -->
|
| 1081 |
+
<h2 id="qwen35">Qwen3.5-4B hybrid (<code>?model=qwen35</code>)</h2>
|
| 1082 |
+
|
| 1083 |
+
<p>
|
| 1084 |
+
The third model, and the first <em>hybrid</em> architecture on the engine: 24 gated-DeltaNet (linear-attention)
|
| 1085 |
+
layers interleaved with 8 gated full-attention layers (attention on every 4th layer). To our knowledge this is
|
| 1086 |
+
the first hand-written-kernel int4 implementation of a gated-DeltaNet hybrid running in a browser. Append
|
| 1087 |
+
<code>?model=qwen35</code> to <code>zero-tvm.html</code> or <code>validate.html</code>;
|
| 1088 |
+
<code>node scripts/download-weights.mjs --model qwen35</code> primes the local dev mirror (~2.6 GB).
|
| 1089 |
+
</p>
|
| 1090 |
+
|
| 1091 |
+
<p>What the hybrid adds over Qwen3-4B:</p>
|
| 1092 |
+
|
| 1093 |
+
<div class="table-scroll"><table>
|
| 1094 |
+
<thead><tr><th></th><th>Qwen3-4B</th><th>Qwen3.5-4B</th></tr></thead>
|
| 1095 |
+
<tbody>
|
| 1096 |
+
<tr><td>Layer stack</td><td>36 Γ attention</td><td>24 Γ gated DeltaNet + 8 Γ gated attention (every 4th layer)</td></tr>
|
| 1097 |
+
<tr><td>Sequence mixer (most layers)</td><td>GQA attention + KV cache</td><td>delta-rule recurrent state (16 k-heads / 32 v-heads, head dims 128, short conv K=4) β no KV cache on those layers</td></tr>
|
| 1098 |
+
<tr><td>Attention layers</td><td>GQA 32/8, head_dim 128, full RoPE</td><td>GQA 16/4, head_dim 256, partial RoPE (64 of 256 dims), sigmoid output gate per head</td></tr>
|
| 1099 |
+
<tr><td>Vocab</td><td>151,936</td><td>248,320 (renumbered specials β the shipped <code>mlc-chat-config.json</code> still lists stale Qwen3 stop ids; stops resolve from <code>tokenizer.json</code>)</td></tr>
|
| 1100 |
+
<tr><td>Weight manifest</td><td><code>ndarray-cache.json</code></td><td><code>tensor-cache.json</code> (MLC renamed it)</td></tr>
|
| 1101 |
+
</tbody>
|
| 1102 |
+
</table></div>
|
| 1103 |
+
|
| 1104 |
+
<p>
|
| 1105 |
+
Measured 2026-07-30 on an Apple M2 Max under the corrected protocol (same session, identical local weight
|
| 1106 |
+
bytes, both engines paying a full prefill on every run): Zero-TVM <strong>65.28 tok/s total</strong>
|
| 1107 |
+
(TTFT 171 ms, decode 73.30) vs WebLLM 0.2.84's prebuilt Qwen3.5-4B at <strong>32.56 tok/s total</strong>
|
| 1108 |
+
(self-reported decode 34.32) β <strong>+100.5% total, +113.6% decode</strong>. This is the one model where
|
| 1109 |
+
first-token latency is roughly a wash rather than a loss (our 171 ms against an implied ~0.2 s from
|
| 1110 |
+
WebLLM's 175β177 tok/s prefill). Static dated text, not synced from <code>bench/results.json</code>;
|
| 1111 |
+
the machine-readable record is <code>bench/results/qwen35-4b.json</code>.
|
| 1112 |
+
</p>
|
| 1113 |
+
|
| 1114 |
+
<div class="callout warning">
|
| 1115 |
+
<div class="callout-title">Read it cautiously β the GDN kernels are still scalar, so decode remains a floor, and the previous number was withdrawn</div>
|
| 1116 |
+
<p>The <strong>"65.67 vs 34.04, +93%" cross-check published on 2026-07-29 is withdrawn</strong> β same defect as the Qwen3 pair: measured after cross-turn prefix reuse shipped and before the bench harness reset it, so only the WebLLM half was paying prefill. The two earlier pairs (53.07 vs 32.36, +64%, from the hybrid perf round; 47.99 vs 31.99, +50%, the v1 floor) predate prefix reuse and were like-for-like β superseded, not defective. Full writeup at the top of BENCH.md. Engine caveats unchanged: the 24 DeltaNet layers run scalar (non-subgroup) kernels, so the decode number is a floor; the input projections are fused 4β1 per GDN layer and prompts prefill in chunks of β€64 since the 2026-07-29 prefill round, but the rest of the Phi-3 fusion story has not been applied to the GDN half. One machine, one pair; full protocol and caveats in BENCH.md.</p>
|
| 1117 |
+
</div>
|
| 1118 |
+
|
| 1119 |
+
<!-- PORTING -->
|
| 1120 |
+
<h2 id="qwen36">Qwen3.6-35B-A3B MoE (<code>?model=qwen36q3</code> / <code>?model=qwen36</code>)</h2>
|
| 1121 |
+
<p>
|
| 1122 |
+
The fourth model, shipped 2026-08-05, and three firsts at once: the first sparse MoE
|
| 1123 |
+
(256 experts, top-8 plus a shared expert on every layer), the first MLX-format
|
| 1124 |
+
checkpoint (affine <code>w = sΒ·q + b</code>, group 64, per-tensor
|
| 1125 |
+
biases, loaded by byte range β a 5.3 GB safetensors shard is never one
|
| 1126 |
+
<code>ArrayBuffer</code>), and the first model here with <strong>no WebLLM build to
|
| 1127 |
+
benchmark against</strong>. <code>?model=qwen36q3</code> is the 3-bit-expert build
|
| 1128 |
+
(~16.4 GB, needs ~20 GB free RAM, ~66 tok/s on a quiet 32 GB
|
| 1129 |
+
M2 Max); <code>?model=qwen36</code> is full 4-bit (~19.5 GB, needs
|
| 1130 |
+
~24 GB free RAM). The MoE block runs in 7 dispatches with the expert index in
|
| 1131 |
+
grid <code>z</code>; every layer is validated against mlx_lm's own modules
|
| 1132 |
+
(<code>npm run test:kernels:real</code>). Full engineering notes live in the repo:
|
| 1133 |
+
<a href="https://github.com/abgnydn/zero-tvm#readme">README</a>,
|
| 1134 |
+
<a href="https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md">BENCH.md</a>,
|
| 1135 |
+
<a href="https://github.com/abgnydn/zero-tvm/blob/main/CLAUDE.md">CLAUDE.md</a>.
|
| 1136 |
+
</p>
|
| 1137 |
+
|
| 1138 |
+
<h2 id="porting">Port to Phi-4-mini or Qwen3</h2>
|
| 1139 |
+
|
| 1140 |
+
<p>Both are available as MLC Q4F16 packages. The Qwen3 port has since landed (see <a href="#qwen3">Qwen3-4B</a> above) β these are the steps it followed, and the recipe for the next model:</p>
|
| 1141 |
+
|
| 1142 |
+
<ol>
|
| 1143 |
+
<li><strong>Update <code>PHI3</code> constants</strong> in <code>compiler.ts</code> β D, HEADS, HEAD_DIM, LAYERS, FFN, VOCAB</li>
|
| 1144 |
+
<li><strong>Check parameter names</strong> β fetch <code>ndarray-cache.json</code> and log all keys. Update <code>weight-loader.ts</code> candidates to match</li>
|
| 1145 |
+
<li><strong>Check for GQA</strong> β if KV heads β Q heads (grouped-query attention), the attention shader needs a small change to repeat KV heads</li>
|
| 1146 |
+
<li><strong>Update chat template</strong> β each model has its own special tokens and prompt format</li>
|
| 1147 |
+
<li><strong>Update HuggingFace base URL</strong> β change <code>PHI3_MODEL_BASE</code> in <code>weight-loader.ts</code></li>
|
| 1148 |
+
</ol>
|
| 1149 |
+
|
| 1150 |
+
<div class="callout tip">
|
| 1151 |
+
<div class="callout-title">Phi-4-mini is the easiest port</div>
|
| 1152 |
+
<p>Same family as Phi-3. MLC package is already available at <code>mlc-ai/Phi-4-mini-instruct-q4f16_1-MLC</code>. Parameter naming is likely identical or very similar.</p>
|
| 1153 |
+
</div>
|
| 1154 |
+
|
| 1155 |
+
<!-- LOCAL WEIGHTS -->
|
| 1156 |
+
<h2 id="local-weights">Local weight serving</h2>
|
| 1157 |
+
|
| 1158 |
+
<p>Run the download script once to save all shards locally. Subsequent page loads are instant (served from localhost, no network).</p>
|
| 1159 |
+
|
| 1160 |
+
<div class="code-block">
|
| 1161 |
+
<div class="code-label">terminal</div>
|
| 1162 |
+
<pre><code>node scripts/download-weights.mjs
|
| 1163 |
+
|
| 1164 |
+
<span class="cm"># Downloads to: .weights-local/Phi-3-mini-4k-instruct-q4f16_1-MLC/</span>
|
| 1165 |
+
<span class="cm"># Served at: /local-weights/Phi-3-mini-4k-instruct-q4f16_1-MLC/</span>
|
| 1166 |
+
<span class="cm"># Size: ~2 GB</span></code></pre>
|
| 1167 |
+
</div>
|
| 1168 |
+
|
| 1169 |
+
<div class="callout danger">
|
| 1170 |
+
<div class="callout-title">Keep index + shards in sync</div>
|
| 1171 |
+
<p>Always download everything together. Never mix a freshly-downloaded <code>ndarray-cache.json</code> with old cached shards β the byte offsets will not match and all weights will be corrupted.</p>
|
| 1172 |
+
</div>
|
| 1173 |
+
|
| 1174 |
+
<!-- DEBUGGING -->
|
| 1175 |
+
<h2 id="debugging">Debugging tips</h2>
|
| 1176 |
+
|
| 1177 |
+
<h3>All output is <code><unk></code></h3>
|
| 1178 |
+
<ul>
|
| 1179 |
+
<li>Weight version mismatch β <code>ndarray-cache.json</code> offsets don't match shard content</li>
|
| 1180 |
+
<li>Buffer aliasing β <code>add_norm</code> dispatched with same buffer as both <code>@1</code> and <code>@4</code></li>
|
| 1181 |
+
<li>Wrong rope binding order β check <code>@0=q_out, @1=k_out, @2=v_out, @3=qkv, @4=posMap</code></li>
|
| 1182 |
+
</ul>
|
| 1183 |
+
|
| 1184 |
+
<h3>Garbage / repetitive output</h3>
|
| 1185 |
+
<ul>
|
| 1186 |
+
<li>Rope bindings are in the wrong order (this was our bug β garbage like <code>-,unlintzegesenma</code>)</li>
|
| 1187 |
+
<li>Wrong uniform values for a shader (K_groups, N, etc.)</li>
|
| 1188 |
+
</ul>
|
| 1189 |
+
|
| 1190 |
+
<h3>WebGPU validation error about aliasing</h3>
|
| 1191 |
+
<ul>
|
| 1192 |
+
<li>Same buffer bound as <code>read_write</code> and <code>read</code> in one dispatch</li>
|
| 1193 |
+
<li>Fix: use ping-pong buffers. Never bind <code>B.residual</code> as both <code>@1</code> and <code>@4</code> to <code>add_norm</code></li>
|
| 1194 |
+
</ul>
|
| 1195 |
+
|
| 1196 |
+
<h3>Model not loading (<code>Weight not found</code>)</h3>
|
| 1197 |
+
<ul>
|
| 1198 |
+
<li>Log all available keys: the weight loader prints them to console on load</li>
|
| 1199 |
+
<li>MLC names differ from HuggingFace standard names (<code>transformer.h.N.mixer.*</code> not <code>model.layers.N.self_attn.*</code>)</li>
|
| 1200 |
+
</ul>
|
| 1201 |
+
|
| 1202 |
+
<!-- BUGS FIXED -->
|
| 1203 |
+
<h2 id="bugs-fixed">Bugs we fixed (and how)</h2>
|
| 1204 |
+
|
| 1205 |
+
<div class="table-scroll"><table>
|
| 1206 |
+
<thead><tr><th>Bug</th><th>Symptom</th><th>Fix</th></tr></thead>
|
| 1207 |
+
<tbody>
|
| 1208 |
+
<tr>
|
| 1209 |
+
<td>Wrong MLC param names</td>
|
| 1210 |
+
<td><code>Weight not found</code> error on load</td>
|
| 1211 |
+
<td>Logged all 325 param names from console, updated candidates to <code>transformer.h.*</code> prefix</td>
|
| 1212 |
+
</tr>
|
| 1213 |
+
<tr>
|
| 1214 |
+
<td>Buffer aliasing in add_norm</td>
|
| 1215 |
+
<td><code><unk></code> Γ 500 at 314 tok/s + WebGPU validation error</td>
|
| 1216 |
+
<td>Added <code>B.residual2</code> (pong buffer), ping-pong with JS variable swap</td>
|
| 1217 |
+
</tr>
|
| 1218 |
+
<tr>
|
| 1219 |
+
<td>Wrong rope binding order</td>
|
| 1220 |
+
<td>Garbage: <code>-,unlintzegesenma\dOCΔalloqueIAL</code> repeated</td>
|
| 1221 |
+
<td>Read rope.wgsl β bindings are <code>@0=q_out @1=k_out @2=v_out @3=qkv @4=posMap</code></td>
|
| 1222 |
+
</tr>
|
| 1223 |
+
<tr>
|
| 1224 |
+
<td>Mixed ndarray-cache.json version</td>
|
| 1225 |
+
<td><code><unk></code> after downloading index locally but using old cached shards</td>
|
| 1226 |
+
<td>Always fetch index and shards from the same source atomically</td>
|
| 1227 |
+
</tr>
|
| 1228 |
+
</tbody>
|
| 1229 |
+
</table></div>
|
| 1230 |
+
|
| 1231 |
+
<!-- VS WEBLLM -->
|
| 1232 |
+
<h2 id="vs-webllm">vs WebLLM</h2>
|
| 1233 |
+
|
| 1234 |
+
<p>Head-to-head on Phi-3-mini-4k-instruct Q4F16_1, same weights, same session, same browser (Chrome 150 with WebGPU), Apple M2 Max, WebLLM 0.2.80 (the Qwen pairs below ran against 0.2.84) β <code>npm run bench</code>, 128-token target Γ 5 runs, median, 2026-07-30 corrected protocol (latest run recorded in <code>bench/results.json</code>). Every run pays a full prefill on both sides, and both metrics are reported: <em>total</em> is wall-clock throughput including prefill, <em>decode</em> excludes it.</p>
|
| 1235 |
+
|
| 1236 |
+
<div class="table-scroll"><table>
|
| 1237 |
+
<thead><tr><th></th><th>Zero-TVM</th><th>WebLLM</th></tr></thead>
|
| 1238 |
+
<tbody>
|
| 1239 |
+
<tr><td>Total throughput (prefill + decode)</td><td><!--bench:zt-->69.55<!--/bench:zt--> tok/s</td><td><!--bench:webllm-->60.0<!--/bench:webllm--> tok/s</td></tr>
|
| 1240 |
+
<tr><td>Decode only</td><td>83.10 tok/s</td><td>63.23 tok/s (self-reported)</td></tr>
|
| 1241 |
+
<tr><td>Time to first token (~35-token prompt)</td><td>291 ms</td><td><strong>~150 ms</strong> (implied from 251 tok/s prefill) β WebLLM ahead</td></tr>
|
| 1242 |
+
<tr><td>Gap</td><td colspan="2"><!--bench:gap-->-16<!--/bench:gap-->% on total relative to WebLLM (negative = Zero-TVM ahead), i.e. +16.0% total and +31.4% decode-only. Same-session pair; cross-session absolute tok/s drifts and the old "β28β¦β31% stable band" was retired on 2026-07-30 (see BENCH.md).</td></tr>
|
| 1243 |
+
<tr><td>Dispatches / token</td><td>260 default (228 with <code>?splitk=0</code>)</td><td>342</td></tr>
|
| 1244 |
+
<tr><td>Distinct shaders</td><td>10 hand-written kernel roles</td><td>~11 TVM-generated on the decode path (85 captured across a session)</td></tr>
|
| 1245 |
+
<tr><td>Shipped JS bundle</td><td>~460 kB / ~126 kB gz (<code>zero-tvm.html</code>)</td><td>~6.0 MB / ~2.2 MB gz (@mlc-ai/web-llm 0.2.84)</td></tr>
|
| 1246 |
+
<tr><td>Bandwidth utilization (total / decode)</td><td>~36% / ~44% of the 191 tok/s ceiling</td><td>~31% / ~33% of the same ceiling</td></tr>
|
| 1247 |
+
<tr><td>Paged attention</td><td>β Hand-written</td><td>β TVM compiled</td></tr>
|
| 1248 |
+
<tr><td>Readable kernels?</td><td>β Yes β every .wgsl file is in the repo</td><td>β No β emitted by the TVM compiler</td></tr>
|
| 1249 |
+
</tbody>
|
| 1250 |
+
</table></div>
|
| 1251 |
+
|
| 1252 |
+
<h3>The corrected protocol (2026-07-30) β and what it invalidated</h3>
|
| 1253 |
+
|
| 1254 |
+
<div class="callout warning">
|
| 1255 |
+
<div class="callout-title">Our half of the A/B had stopped paying prefill</div>
|
| 1256 |
+
<p><code>bench()</code> in <code>src/zero-tvm/bench-console.ts</code> looped its runs against the same prompt and never called <code>engine.resetKVTracking()</code> β while <code>benchPrefill()</code>, <code>specSim()</code> and <code>validate.ts</code> all did. That was harmless until cross-turn prefix reuse shipped on 2026-07-29 (PR #24). After it, runs 2..N of every bench found the whole prompt already absorbed and prefilled exactly <strong>one</strong> token, while the WebLLM half β a fresh chat completion per run β kept paying a full prefill inside its wall clock. The two halves were measuring different work.</p>
|
| 1257 |
+
<p><strong>Withdrawn as a result:</strong> the Qwen3-4B "75.74 / 43.75, +73.1%" and Qwen3.5-4B "65.67 / 34.04, +92.9%" pairs, both 2026-07-29. Everything earlier predates prefix reuse and was like-for-like. <strong>Fixed:</strong> <code>bench()</code> resets before every run, both halves split TTFT from decode, and WebLLM's own per-run decode/prefill rates are captured instead of logged once and discarded. Prior published numbers stay in BENCH.md as dated history with the defect explained in place β nothing was silently overwritten.</p>
|
| 1258 |
+
</div>
|
| 1259 |
+
|
| 1260 |
+
<h3>The advantage grows with architecture recency</h3>
|
| 1261 |
+
|
| 1262 |
+
<p>Monotonic on both metrics across the three models then shipped, all measured 2026-07-30 on the same machine (Qwen3.6-35B-A3B came later and has no WebLLM baseline to pair against β see BENCH.md):</p>
|
| 1263 |
+
|
| 1264 |
+
<div class="table-scroll"><table>
|
| 1265 |
+
<thead><tr><th>Model</th><th>Architecture released</th><th>Ξ total</th><th>Ξ decode</th></tr></thead>
|
| 1266 |
+
<tbody>
|
| 1267 |
+
<tr><td>Phi-3-mini</td><td>2024</td><td>+16.0%</td><td>+31.4%</td></tr>
|
| 1268 |
+
<tr><td>Qwen3-4B</td><td>2025</td><td>+31.7%</td><td>+58.0%</td></tr>
|
| 1269 |
+
<tr><td>Qwen3.5-4B</td><td>2026</td><td>+100.5%</td><td>+113.6%</td></tr>
|
| 1270 |
+
</tbody>
|
| 1271 |
+
</table></div>
|
| 1272 |
+
|
| 1273 |
+
<p>The reading that fits: compiler stacks have had less time to tune newer architectures, so there is more headroom for a hand-written kernel set to take. That is an <strong>observation across three points</strong>, not a proven law β one machine, one browser, one day, no mechanism isolated, no control for how differently each model stresses the two engines β and the baseline is not constant across the rows (WebLLM 0.2.80 for Phi-3, 0.2.84 for both Qwen rows). Worth testing on a fourth model.</p>
|
| 1274 |
+
|
| 1275 |
+
<h3>Where we lose: time to first token on short prompts</h3>
|
| 1276 |
+
|
| 1277 |
+
<div class="callout warning">
|
| 1278 |
+
<div class="callout-title">WebLLM reaches the first token sooner than we do</div>
|
| 1279 |
+
<p>WebLLM's self-reported prefill runs at 251 tok/s on Phi-3 and 263β271 tok/s on Qwen3-4B. Against the ~35-token bench prompt that implies a TTFT of roughly <strong>150 ms</strong> on Phi-3, where we measure <strong>291 ms</strong>; on Qwen3-4B our <strong>453 ms</strong> is worse still. Only Qwen3.5-4B is a wash (our 171 ms vs an implied ~0.2 s). Stated plainly: <strong>we win sustained decode decisively and lose the first-token sprint on short inputs.</strong></p>
|
| 1280 |
+
<p>It is specifically a <em>short</em>-prompt weakness. Chunked prefill measures 202 tok/s on an 816-token prompt (2026-07-29), and cross-turn prefix reuse removes prefill entirely on follow-up turns. What is not competitive is the fixed cost of a short, cold prefill with nothing cached to reuse. It is the top open item on BENCH.md's levers list.</p>
|
| 1281 |
+
</div>
|
| 1282 |
+
|
| 1283 |
+
<h3>How the result flipped (22% behind → +16% total / +31% decode ahead)</h3>
|
| 1284 |
+
<p>An earlier head-to-head (M2 Pro, 2026-06) read 42.14 vs ~51.5 tok/s β Zero-TVM 22% <em>behind</em>. Both the hardware and the engine changed since, so the two Zero-TVM numbers are not a pure optimization delta; the same-session, same-machine WebLLM number is the valid comparator for the current Zero-TVM median (both in the table above). What changed in the engine:</p>
|
| 1285 |
+
<ul>
|
| 1286 |
+
<li><strong>Correctness fixes</strong>. f32 accumulation in the fused FFN, a workgroup-barrier fix in attention, and a decode off-by-one fix β the old number understates even the old hardware.</li>
|
| 1287 |
+
<li><strong>vec4 loads, now default</strong>. Re-declaring weight + activation buffers as <code>array<vec4<u32>></code> in the int4 matmuls and <code>qkv_fused</code> measured +7.1% on M2 Max; opt out with <code>?vec4=0</code> / <code>?vec4qkv=0</code>.</li>
|
| 1288 |
+
<li><strong>Tiled + subgroup kernels</strong>. The 4-row tiled subgroup FFN and the <code>_sg</code> matmul/attention/argmax variants are the default path on Apple sg32 hardware.</li>
|
| 1289 |
+
</ul>
|
| 1290 |
+
<p>Still default since 2026-07-27 (`?splitk=0` to disable): split-K attention (<code>?splitk=N</code>, ~+3% at short context, needs a long-context A/B); the best measured opt-in config of that era (<code>?vec4=1&vec4qkv=1&splitk=8</code>, 68.36 tok/s on 2026-07-25) is now simply the default path. Falsified and not shipped: FFN prologue fusion (<code>?fuseprologue=1</code>, β13.7% on M2 Max). Dispatch overhead was never the gap β Zero-TVM already submitted fewer dispatches per token (260 vs 342) when it was slower. Full A/B tables in BENCH.md.</p>
|
| 1291 |
+
|
| 1292 |
+
<h3>The memory-bandwidth ceiling</h3>
|
| 1293 |
+
<p>Phi-3-mini Q4F16_1 touches ~2.09 GB of weights per decode token (the weight-shape table above, summed: 32 layers plus the LM head; the embedding contributes one row). On M2 Max's 400 GB/s memory bus that's ~5.2 ms/token, or ~191 tok/s theoretical max. Zero-TVM's measured decode-only median sits at roughly 44% of that ceiling (36% on total, which pays prefill too); WebLLM's at roughly 33% decode-only / 31% total (exact medians in the table above). Neither engine can exceed the ceiling without changing weight layout or quantization (int8 KV shaves a bit, which is why it's exposed behind a flag).</p>
|
| 1294 |
+
|
| 1295 |
+
</main>
|
| 1296 |
+
</div>
|
| 1297 |
+
|
| 1298 |
+
<script>
|
| 1299 |
+
// Highlight active sidebar link on scroll
|
| 1300 |
+
const links = document.querySelectorAll('.sidebar-link[href^="#"]')
|
| 1301 |
+
const observer = new IntersectionObserver((entries) => {
|
| 1302 |
+
for (const entry of entries) {
|
| 1303 |
+
if (entry.isIntersecting) {
|
| 1304 |
+
const id = entry.target.id
|
| 1305 |
+
links.forEach(l => {
|
| 1306 |
+
l.classList.toggle('active', l.getAttribute('href') === '#' + id)
|
| 1307 |
+
})
|
| 1308 |
+
}
|
| 1309 |
+
}
|
| 1310 |
+
}, { rootMargin: '-60px 0px -60% 0px' })
|
| 1311 |
+
|
| 1312 |
+
document.querySelectorAll('[id]').forEach(el => observer.observe(el))
|
| 1313 |
+
</script>
|
| 1314 |
+
|
| 1315 |
+
</body>
|
| 1316 |
+
</html>
|
entrance/backring.svg
ADDED
|
|
entrance/banner.svg
ADDED
|
|
entrance/bg.svg
ADDED
|
|
entrance/border.svg
ADDED
|
|
entrance/column.svg
ADDED
|
|
entrance/corner.svg
ADDED
|
|
entrance/divider.svg
ADDED
|
|
entrance/pframe.svg
ADDED
|
|
entrance/ring.svg
ADDED
|
|
entrance/spires.svg
ADDED
|
|
favicon.svg
ADDED
|
|
fonts.css
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Self-hosted. The site's claim is that nothing leaves your machine;
|
| 2 |
+
a webfont request to Google would leak every visitor's IP and make
|
| 3 |
+
that claim false on the landing page itself. 292 KB, 7 files. */
|
| 4 |
+
@font-face {
|
| 5 |
+
font-family: 'Fraunces';
|
| 6 |
+
font-style: normal;
|
| 7 |
+
font-weight: 400;
|
| 8 |
+
font-display: swap;
|
| 9 |
+
src: url('/fonts/f2-fraunce.woff2') format('woff2');
|
| 10 |
+
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
| 11 |
+
}
|
| 12 |
+
@font-face {
|
| 13 |
+
font-family: 'Fraunces';
|
| 14 |
+
font-style: normal;
|
| 15 |
+
font-weight: 400;
|
| 16 |
+
font-display: swap;
|
| 17 |
+
src: url('/fonts/f3-fraunce.woff2') format('woff2');
|
| 18 |
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
| 19 |
+
}
|
| 20 |
+
@font-face {
|
| 21 |
+
font-family: 'Fraunces';
|
| 22 |
+
font-style: normal;
|
| 23 |
+
font-weight: 400;
|
| 24 |
+
font-display: swap;
|
| 25 |
+
src: url('/fonts/f1-fraunce.woff2') format('woff2');
|
| 26 |
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
| 27 |
+
}
|
| 28 |
+
@font-face {
|
| 29 |
+
font-family: 'Fraunces';
|
| 30 |
+
font-style: normal;
|
| 31 |
+
font-weight: 600;
|
| 32 |
+
font-display: swap;
|
| 33 |
+
src: url('/fonts/f2-fraunce.woff2') format('woff2');
|
| 34 |
+
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
| 35 |
+
}
|
| 36 |
+
@font-face {
|
| 37 |
+
font-family: 'Fraunces';
|
| 38 |
+
font-style: normal;
|
| 39 |
+
font-weight: 600;
|
| 40 |
+
font-display: swap;
|
| 41 |
+
src: url('/fonts/f3-fraunce.woff2') format('woff2');
|
| 42 |
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
| 43 |
+
}
|
| 44 |
+
@font-face {
|
| 45 |
+
font-family: 'Fraunces';
|
| 46 |
+
font-style: normal;
|
| 47 |
+
font-weight: 600;
|
| 48 |
+
font-display: swap;
|
| 49 |
+
src: url('/fonts/f1-fraunce.woff2') format('woff2');
|
| 50 |
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
| 51 |
+
}
|
| 52 |
+
@font-face {
|
| 53 |
+
font-family: 'Spline Sans';
|
| 54 |
+
font-style: normal;
|
| 55 |
+
font-weight: 400;
|
| 56 |
+
font-display: swap;
|
| 57 |
+
src: url('/fonts/f5-plinean.woff2') format('woff2');
|
| 58 |
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
| 59 |
+
}
|
| 60 |
+
@font-face {
|
| 61 |
+
font-family: 'Spline Sans';
|
| 62 |
+
font-style: normal;
|
| 63 |
+
font-weight: 400;
|
| 64 |
+
font-display: swap;
|
| 65 |
+
src: url('/fonts/f4-plinean.woff2') format('woff2');
|
| 66 |
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
| 67 |
+
}
|
| 68 |
+
@font-face {
|
| 69 |
+
font-family: 'Spline Sans';
|
| 70 |
+
font-style: normal;
|
| 71 |
+
font-weight: 500;
|
| 72 |
+
font-display: swap;
|
| 73 |
+
src: url('/fonts/f5-plinean.woff2') format('woff2');
|
| 74 |
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
| 75 |
+
}
|
| 76 |
+
@font-face {
|
| 77 |
+
font-family: 'Spline Sans';
|
| 78 |
+
font-style: normal;
|
| 79 |
+
font-weight: 500;
|
| 80 |
+
font-display: swap;
|
| 81 |
+
src: url('/fonts/f4-plinean.woff2') format('woff2');
|
| 82 |
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
| 83 |
+
}
|
| 84 |
+
@font-face {
|
| 85 |
+
font-family: 'Spline Sans';
|
| 86 |
+
font-style: normal;
|
| 87 |
+
font-weight: 600;
|
| 88 |
+
font-display: swap;
|
| 89 |
+
src: url('/fonts/f5-plinean.woff2') format('woff2');
|
| 90 |
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
| 91 |
+
}
|
| 92 |
+
@font-face {
|
| 93 |
+
font-family: 'Spline Sans';
|
| 94 |
+
font-style: normal;
|
| 95 |
+
font-weight: 600;
|
| 96 |
+
font-display: swap;
|
| 97 |
+
src: url('/fonts/f4-plinean.woff2') format('woff2');
|
| 98 |
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
| 99 |
+
}
|
| 100 |
+
@font-face {
|
| 101 |
+
font-family: 'Spline Sans Mono';
|
| 102 |
+
font-style: normal;
|
| 103 |
+
font-weight: 400;
|
| 104 |
+
font-display: swap;
|
| 105 |
+
src: url('/fonts/f7-plineanmono.woff2') format('woff2');
|
| 106 |
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
| 107 |
+
}
|
| 108 |
+
@font-face {
|
| 109 |
+
font-family: 'Spline Sans Mono';
|
| 110 |
+
font-style: normal;
|
| 111 |
+
font-weight: 400;
|
| 112 |
+
font-display: swap;
|
| 113 |
+
src: url('/fonts/f6-plineanmono.woff2') format('woff2');
|
| 114 |
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
| 115 |
+
}
|
| 116 |
+
@font-face {
|
| 117 |
+
font-family: 'Spline Sans Mono';
|
| 118 |
+
font-style: normal;
|
| 119 |
+
font-weight: 500;
|
| 120 |
+
font-display: swap;
|
| 121 |
+
src: url('/fonts/f7-plineanmono.woff2') format('woff2');
|
| 122 |
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
| 123 |
+
}
|
| 124 |
+
@font-face {
|
| 125 |
+
font-family: 'Spline Sans Mono';
|
| 126 |
+
font-style: normal;
|
| 127 |
+
font-weight: 500;
|
| 128 |
+
font-display: swap;
|
| 129 |
+
src: url('/fonts/f6-plineanmono.woff2') format('woff2');
|
| 130 |
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
| 131 |
+
}
|
fonts/f1-fraunce.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:48282a415ec22e31beaf0a0666e6fae0c8cbddcd0b1f6e729f27c3ade8a64e43
|
| 3 |
+
size 67388
|
fonts/f2-fraunce.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7e3c04662669cd2b8ee672e4e5d63146d21f6aa36654b211718af5fe0bb3bd51
|
| 3 |
+
size 19748
|
fonts/f3-fraunce.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f120089b9440f3e35a980a2137347b1851ad46ee4b8ad5c63b6c07e053fa40e2
|
| 3 |
+
size 59540
|
fonts/f4-plinean.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:14ed7b4db18fb8fd77f252da580df74f457f81a788cff0b71f357e882279156c
|
| 3 |
+
size 57844
|
fonts/f5-plinean.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d13b339c84af67cc3b402cc08f2f53e08f29b65b9ca5ac7b729b5b4243a02638
|
| 3 |
+
size 21048
|
fonts/f6-plineanmono.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2b193a4f0fef3fc4c6a3fa756bb5dab6c10b489acd5e0718736847755077e9ec
|
| 3 |
+
size 36528
|
fonts/f7-plineanmono.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bde42c142265c31a450781d82cc72e1859bd42cba8e438fb96f77f1d69ebc8d6
|
| 3 |
+
size 20748
|
index.html
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>zero-tvm β LLM inference in the browser, on hand-written WGSL</title>
|
| 7 |
+
<meta name="description" content="zero-tvm is an LLM inference engine written by hand in WGSL and TypeScript. It runs entirely in the browser: weights download once, cache locally, and inference runs on your own GPU.">
|
| 8 |
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
| 9 |
+
<link rel="canonical" href="https://zerotvm.com/">
|
| 10 |
+
|
| 11 |
+
<meta property="og:type" content="website">
|
| 12 |
+
<meta property="og:url" content="https://zerotvm.com/">
|
| 13 |
+
<meta property="og:title" content="zero-tvm β LLM inference in the browser, on hand-written WGSL">
|
| 14 |
+
<meta property="og:description" content="An LLM inference engine written by hand in WGSL and TypeScript. Weights download once, cache locally, and inference runs on your own GPU.">
|
| 15 |
+
<!-- The canonical URL is the one that actually gets posted, and it was the
|
| 16 |
+
only page declaring summary_large_image without an image to put in it β
|
| 17 |
+
so every share of https://zerotvm.com/ unfurled as a bare text card
|
| 18 |
+
while /docs.html unfurled richly. og.png ships already. -->
|
| 19 |
+
<meta property="og:image" content="https://zerotvm.com/og.png">
|
| 20 |
+
<meta name="twitter:card" content="summary_large_image">
|
| 21 |
+
<meta name="twitter:image" content="https://zerotvm.com/og.png">
|
| 22 |
+
<meta name="twitter:title" content="zero-tvm β LLM inference in the browser, on hand-written WGSL">
|
| 23 |
+
<meta name="twitter:description" content="An LLM inference engine written by hand in WGSL and TypeScript. Weights download once, cache locally, and inference runs on your own GPU.">
|
| 24 |
+
|
| 25 |
+
<link rel="preload" href="/entrance/bg.svg" as="image" type="image/svg+xml">
|
| 26 |
+
<link rel="preload" href="/entrance/ring.svg" as="image" type="image/svg+xml">
|
| 27 |
+
<link rel="preload" href="/fonts/f3-fraunce.woff2" as="font" type="font/woff2" crossorigin>
|
| 28 |
+
<link rel="preload" href="/fonts/f4-plinean.woff2" as="font" type="font/woff2" crossorigin>
|
| 29 |
+
<link rel="stylesheet" href="/fonts.css">
|
| 30 |
+
<link rel="stylesheet" href="/tokens.css">
|
| 31 |
+
<!-- chat-ui.css: the shared message surface β ENTER opens the chat IN the
|
| 32 |
+
entrance (landing-chat.ts), so this page renders bubbles and the
|
| 33 |
+
composer too. Before landing.css, which carries the panel chrome. -->
|
| 34 |
+
<link rel="stylesheet" href="/chat-ui.css">
|
| 35 |
+
<link rel="stylesheet" href="/landing.css">
|
| 36 |
+
|
| 37 |
+
<script type="application/ld+json">
|
| 38 |
+
{
|
| 39 |
+
"@context": "https://schema.org",
|
| 40 |
+
"@type": "SoftwareSourceCode",
|
| 41 |
+
"name": "zero-tvm",
|
| 42 |
+
"url": "https://zerotvm.com/",
|
| 43 |
+
"description": "An LLM inference engine written by hand in WGSL and TypeScript, running entirely in the browser.",
|
| 44 |
+
"programmingLanguage": [
|
| 45 |
+
"WGSL",
|
| 46 |
+
"TypeScript"
|
| 47 |
+
],
|
| 48 |
+
"codeRepository": "https://github.com/abgnydn/zero-tvm",
|
| 49 |
+
"license": "https://opensource.org/licenses/MIT",
|
| 50 |
+
"isPartOf": {
|
| 51 |
+
"@type": "Collection",
|
| 52 |
+
"name": "WebGPU research line",
|
| 53 |
+
"url": "https://kernelfusion.dev"
|
| 54 |
+
},
|
| 55 |
+
"author": {
|
| 56 |
+
"@type": "Person",
|
| 57 |
+
"name": "Ahmet BarΔ±Ε GΓΌnaydΔ±n",
|
| 58 |
+
"url": "https://zerotvm.com/",
|
| 59 |
+
"sameAs": [
|
| 60 |
+
"https://barisgunaydin.com",
|
| 61 |
+
"https://github.com/abgnydn",
|
| 62 |
+
"https://huggingface.co/abgunaydin",
|
| 63 |
+
"https://x.com/abgnydn_",
|
| 64 |
+
"https://www.linkedin.com/in/abgnydn/"
|
| 65 |
+
]
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
</script>
|
| 69 |
+
<script type="module" crossorigin src="/assets/index-BqNbylMY24.js"></script>
|
| 70 |
+
<link rel="modulepreload" crossorigin href="/assets/model-registry-BoK-ObgACo.js">
|
| 71 |
+
<link rel="modulepreload" crossorigin href="/assets/preload-helper-ckwbz45pO3.js">
|
| 72 |
+
<link rel="modulepreload" crossorigin href="/assets/mascot-BCKWddkWrV.js">
|
| 73 |
+
<link rel="modulepreload" crossorigin href="/assets/feats-dlBfBxqpI8.js">
|
| 74 |
+
</head>
|
| 75 |
+
<body>
|
| 76 |
+
|
| 77 |
+
<nav class="nav">
|
| 78 |
+
<div class="wrap nav-in">
|
| 79 |
+
<a href="/" class="nav-logo">zero<span>-tvm</span></a>
|
| 80 |
+
<div class="nav-links">
|
| 81 |
+
<a href="#models">Models</a>
|
| 82 |
+
<!-- "Rooms" pointed at share.html's bare host page (a floating modal
|
| 83 |
+
over black). Hosting lives in the entrance now β enter a character,
|
| 84 |
+
then β Room. share.html remains the GUEST surface links point at,
|
| 85 |
+
and the footer still lists it. -->
|
| 86 |
+
<!-- "Agent" was here. agent-host.html only works against a local
|
| 87 |
+
agent-server on 127.0.0.1:8017, so a public visitor paid a ~2 GB
|
| 88 |
+
download for a page that then sat on "bootingβ¦" forever. It is a
|
| 89 |
+
repo workflow (`npm run agent`), documented in the README, not a
|
| 90 |
+
destination on the site. -->
|
| 91 |
+
<a href="docs.html#vs-webllm">Proof</a>
|
| 92 |
+
<a href="docs.html">Docs</a>
|
| 93 |
+
<!-- "Open chat β" left with the in-place chat: the page below IS the
|
| 94 |
+
chat entrance, and the CTA duplicated ENTER while routing through
|
| 95 |
+
the old standalone gate. zero-tvm.html remains the deep-link
|
| 96 |
+
surface (footer, docs, ?model= links). -->
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</nav>
|
| 100 |
+
|
| 101 |
+
<section id="models">
|
| 102 |
+
<!-- The game entrance. Full-viewport character select β landing.ts renders
|
| 103 |
+
the whole screen (splash, stage, roster, sheet) into this root. Every
|
| 104 |
+
figure and number still comes from the registry; the scroll below keeps
|
| 105 |
+
the essay and the kernel window for the reader who wants the why. -->
|
| 106 |
+
<div id="model-browser" class="cs-root"></div>
|
| 107 |
+
</section>
|
| 108 |
+
|
| 109 |
+
<!-- The page IS the character select now β nav above, footer below,
|
| 110 |
+
nothing between. The essay, the kernel window and the deep copy left
|
| 111 |
+
with the 2026-08-15 entrance redesign; docs.html carries the why. -->
|
| 112 |
+
|
| 113 |
+
<footer>
|
| 114 |
+
<div class="wrap">
|
| 115 |
+
<div class="foot-grid">
|
| 116 |
+
<div>
|
| 117 |
+
<a href="/" class="nav-logo">zero<span>-tvm</span></a>
|
| 118 |
+
<p class="foot-about">
|
| 119 |
+
An LLM inference engine written by hand in WGSL and TypeScript. Part
|
| 120 |
+
of a research line on WebGPU kernels by Ahmet BarΔ±Ε GΓΌnaydΔ±n.
|
| 121 |
+
</p>
|
| 122 |
+
</div>
|
| 123 |
+
<div>
|
| 124 |
+
<h4>This site</h4>
|
| 125 |
+
<ul>
|
| 126 |
+
<li><a href="zero-tvm.html">Chat</a></li>
|
| 127 |
+
<li><a href="share.html">Host a room</a></li>
|
| 128 |
+
<li><a href="docs.html">Docs</a></li>
|
| 129 |
+
<li><a href="docs.html#vs-webllm">Proof</a></li>
|
| 130 |
+
<li><a href="validate.html">Validate</a></li>
|
| 131 |
+
</ul>
|
| 132 |
+
</div>
|
| 133 |
+
<div>
|
| 134 |
+
<h4>Research line</h4>
|
| 135 |
+
<ul id="sibling-links">
|
| 136 |
+
<li><a href="https://kernelfusion.dev">kernelfusion.dev</a></li>
|
| 137 |
+
<li><a href="https://github.com/abgnydn">github.com/abgnydn</a></li>
|
| 138 |
+
<li><a href="https://huggingface.co/abgunaydin">huggingface.co/abgunaydin</a></li>
|
| 139 |
+
</ul>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="foot-bottom">
|
| 143 |
+
<span>MIT licensed Β· weights are fetched once from HuggingFace and cached in your browser; prompts and generation stay on your machine in the chat. In a room, a guest’s prompts run on the host’s GPU.</span>
|
| 144 |
+
<a href="https://github.com/abgnydn/zero-tvm">github.com/abgnydn/zero-tvm β</a>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</footer>
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
</body>
|
| 151 |
+
</html>
|
landing.css
ADDED
|
@@ -0,0 +1,1442 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* LANDING β Foundry chassis. docs/SITE_REDESIGN.md.
|
| 2 |
+
No figure is written here. Anything numeric on this page is rendered from
|
| 3 |
+
model-registry.ts at runtime or it is absent, so adding a model updates the
|
| 4 |
+
page by itself and a stale number cannot be committed. */
|
| 5 |
+
|
| 6 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 7 |
+
|
| 8 |
+
body {
|
| 9 |
+
margin: 0;
|
| 10 |
+
background: var(--bg);
|
| 11 |
+
color: var(--text);
|
| 12 |
+
font-family: var(--body);
|
| 13 |
+
font-size: var(--step-0);
|
| 14 |
+
line-height: 1.65;
|
| 15 |
+
-webkit-font-smoothing: antialiased;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
a { color: var(--accent); text-decoration: none; }
|
| 19 |
+
a:hover { color: var(--accent-hi); }
|
| 20 |
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--rad-s); }
|
| 21 |
+
|
| 22 |
+
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }
|
| 23 |
+
|
| 24 |
+
/* ββ eyebrow βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 25 |
+
.eyebrow {
|
| 26 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 27 |
+
font-family: var(--mono); font-size: var(--step--1);
|
| 28 |
+
letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim);
|
| 29 |
+
}
|
| 30 |
+
.pulse {
|
| 31 |
+
width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
|
| 32 |
+
box-shadow: 0 0 0 0 var(--accent-dim); animation: pulse 2.4s ease-out infinite;
|
| 33 |
+
}
|
| 34 |
+
@keyframes pulse {
|
| 35 |
+
0% { box-shadow: 0 0 0 0 #f0a86066; }
|
| 36 |
+
70% { box-shadow: 0 0 0 9px #f0a86000; }
|
| 37 |
+
100% { box-shadow: 0 0 0 0 #f0a86000; }
|
| 38 |
+
}
|
| 39 |
+
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; } }
|
| 40 |
+
|
| 41 |
+
/* ββ nav βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 42 |
+
.nav {
|
| 43 |
+
position: sticky; top: 0; z-index: 40;
|
| 44 |
+
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
| 45 |
+
backdrop-filter: blur(10px);
|
| 46 |
+
border-bottom: 1px solid var(--border);
|
| 47 |
+
}
|
| 48 |
+
.nav-in { display: flex; align-items: center; gap: 28px; height: 60px; }
|
| 49 |
+
.nav-logo {
|
| 50 |
+
font-family: var(--mono); font-weight: 500; color: var(--text);
|
| 51 |
+
letter-spacing: -0.02em; margin-right: auto;
|
| 52 |
+
}
|
| 53 |
+
.nav-logo span { color: var(--accent); }
|
| 54 |
+
.nav a:not(.nav-logo):not(.nav-cta) { color: var(--muted); font-size: var(--step--1); }
|
| 55 |
+
.nav a:not(.nav-logo):not(.nav-cta):hover { color: var(--text); }
|
| 56 |
+
.nav-cta {
|
| 57 |
+
font-size: var(--step--1); padding: 7px 15px; border-radius: var(--rad-s);
|
| 58 |
+
border: 1px solid var(--accent); color: var(--accent);
|
| 59 |
+
}
|
| 60 |
+
.nav-cta:hover { background: var(--accent); color: var(--bg); }
|
| 61 |
+
.nav-links { display: flex; gap: 22px; align-items: center; }
|
| 62 |
+
/* On phones the nav used to collapse to ONLY "Open chat" β the single action
|
| 63 |
+
that costs gigabytes β while Docs and everything explanatory vanished. Docs
|
| 64 |
+
stays; the heavyweight links are the ones that can go. */
|
| 65 |
+
@media (max-width: 720px) { .nav-links a:not(.nav-cta):not([href="docs.html"]) { display: none; } }
|
| 66 |
+
|
| 67 |
+
/* ββ hero ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 68 |
+
.hero { padding: 84px 0 64px; }
|
| 69 |
+
.hero h1 {
|
| 70 |
+
font-family: var(--display);
|
| 71 |
+
font-size: var(--step-3);
|
| 72 |
+
font-weight: 400;
|
| 73 |
+
line-height: 1.08;
|
| 74 |
+
letter-spacing: -0.02em;
|
| 75 |
+
margin: 20px 0 0;
|
| 76 |
+
text-wrap: balance;
|
| 77 |
+
}
|
| 78 |
+
.hero h1 em { font-style: italic; color: var(--accent); }
|
| 79 |
+
.hero p {
|
| 80 |
+
color: var(--muted); max-width: 62ch; margin: 20px 0 0; font-size: var(--step-1);
|
| 81 |
+
line-height: 1.6;
|
| 82 |
+
}
|
| 83 |
+
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 28px; }
|
| 84 |
+
.btn {
|
| 85 |
+
font-family: var(--body); font-size: var(--step-0);
|
| 86 |
+
padding: 11px 20px; border-radius: var(--rad-s); border: 1px solid var(--border-hi);
|
| 87 |
+
color: var(--text); background: transparent; cursor: pointer;
|
| 88 |
+
}
|
| 89 |
+
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
| 90 |
+
.btn-primary { background: var(--accent); border-color: var(--accent); color: #1a1206; font-weight: 500; }
|
| 91 |
+
.btn-primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: #1a1206; }
|
| 92 |
+
|
| 93 |
+
/* ββ kernel window βββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 94 |
+
.kw {
|
| 95 |
+
margin-top: 44px; border: 1px solid var(--border); border-radius: var(--rad);
|
| 96 |
+
background: var(--surface); overflow: hidden;
|
| 97 |
+
}
|
| 98 |
+
.kw-bar {
|
| 99 |
+
display: flex; align-items: center; gap: 10px;
|
| 100 |
+
padding: 0 14px; height: 40px; border-bottom: 1px solid var(--border);
|
| 101 |
+
background: var(--surface-2);
|
| 102 |
+
}
|
| 103 |
+
.kw-dots { display: flex; gap: 6px; margin-right: 6px; }
|
| 104 |
+
.kw-dots i { width: 10px; height: 10px; border-radius: 50%; background: var(--border-hi); }
|
| 105 |
+
.kw-tabs { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; }
|
| 106 |
+
.kw-tabs::-webkit-scrollbar { display: none; }
|
| 107 |
+
.kw-tab {
|
| 108 |
+
font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
|
| 109 |
+
background: none; border: 0; padding: 6px 11px; border-radius: var(--rad-s);
|
| 110 |
+
cursor: pointer; white-space: nowrap;
|
| 111 |
+
}
|
| 112 |
+
.kw-tab:hover { color: var(--muted); }
|
| 113 |
+
.kw-tab[aria-selected="true"] { color: var(--accent); background: var(--accent-dim); }
|
| 114 |
+
.kw-body { overflow-x: auto; }
|
| 115 |
+
.kw-body pre {
|
| 116 |
+
margin: 0; padding: 20px 22px;
|
| 117 |
+
font-family: var(--mono); font-size: var(--step--1); line-height: 1.7;
|
| 118 |
+
color: var(--muted);
|
| 119 |
+
}
|
| 120 |
+
.kw-body pre[hidden] { display: none; }
|
| 121 |
+
.kw-foot {
|
| 122 |
+
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
| 123 |
+
padding: 10px 16px; border-top: 1px solid var(--border);
|
| 124 |
+
font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
|
| 125 |
+
flex-wrap: wrap;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/* WGSL syntax β one hue family, so the code reads as material rather than
|
| 129 |
+
as a rainbow. Comments recede; the accent is spent on identifiers. */
|
| 130 |
+
.t-key { color: #c58bff; }
|
| 131 |
+
.t-fn { color: var(--accent-hi); }
|
| 132 |
+
.t-num { color: var(--ok); }
|
| 133 |
+
.t-str { color: #9fd3ff; }
|
| 134 |
+
.t-com { color: var(--dim); font-style: italic; }
|
| 135 |
+
.t-attr { color: #7e8aa0; }
|
| 136 |
+
.t-type { color: #8fd0ff; }
|
| 137 |
+
|
| 138 |
+
/* ββ capability band βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 139 |
+
.band { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border);
|
| 140 |
+
border: 1px solid var(--border); border-radius: var(--rad); overflow: hidden; }
|
| 141 |
+
@media (max-width: 840px) { .band { grid-template-columns: repeat(2, 1fr); } }
|
| 142 |
+
@media (max-width: 480px) { .band { grid-template-columns: 1fr; } }
|
| 143 |
+
.band a { background: var(--surface); padding: 22px; display: block; color: inherit; }
|
| 144 |
+
.band a:hover { background: var(--surface-2); }
|
| 145 |
+
.band h3 {
|
| 146 |
+
font-family: var(--mono); font-size: var(--step--1); text-transform: uppercase;
|
| 147 |
+
letter-spacing: 0.08em; color: var(--accent); margin: 0 0 8px; font-weight: 500;
|
| 148 |
+
}
|
| 149 |
+
.band p { margin: 0; color: var(--muted); font-size: var(--step--1); line-height: 1.6; }
|
| 150 |
+
|
| 151 |
+
/* ββ sections ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 152 |
+
section { padding: 72px 0; }
|
| 153 |
+
.sec-head { margin-bottom: 32px; }
|
| 154 |
+
.sec-head h2 {
|
| 155 |
+
font-family: var(--display); font-weight: 400; font-size: var(--step-2);
|
| 156 |
+
margin: 12px 0 0; letter-spacing: -0.01em;
|
| 157 |
+
}
|
| 158 |
+
.sec-head p { color: var(--muted); margin: 12px 0 0; max-width: 66ch; }
|
| 159 |
+
|
| 160 |
+
/* ββ model browser βββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 161 |
+
/* The container IS the keyboard interface β index.html tells the reader the
|
| 162 |
+
arrow keys move between models, and this is the element that must be
|
| 163 |
+
focused for that to work. Removing its ring left no way to tell. */
|
| 164 |
+
#model-browser:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
| 165 |
+
.mb-stage { display: flex; align-items: center; gap: 8px; }
|
| 166 |
+
.mb-arrow {
|
| 167 |
+
flex: 0 0 auto; width: 42px; height: 42px; border-radius: 50%;
|
| 168 |
+
background: var(--surface); border: 1px solid var(--border); color: var(--muted);
|
| 169 |
+
font-size: 22px; line-height: 1; cursor: pointer; font-family: var(--body);
|
| 170 |
+
}
|
| 171 |
+
.mb-arrow:hover { border-color: var(--accent); color: var(--accent); }
|
| 172 |
+
.mb-slide {
|
| 173 |
+
flex: 1 1 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 32px;
|
| 174 |
+
align-items: center; min-height: 380px;
|
| 175 |
+
border: 1px solid var(--border); border-radius: var(--rad);
|
| 176 |
+
background: linear-gradient(180deg, var(--surface-2), var(--surface));
|
| 177 |
+
padding: 28px 32px;
|
| 178 |
+
}
|
| 179 |
+
@media (max-width: 780px) {
|
| 180 |
+
.mb-slide { grid-template-columns: 1fr; text-align: center; min-height: 0; }
|
| 181 |
+
.mb-stats { justify-items: center; }
|
| 182 |
+
.mb-variants, .mb-info { justify-content: center; align-items: center; }
|
| 183 |
+
}
|
| 184 |
+
.mb-art { position: relative; padding-bottom: 66px; }
|
| 185 |
+
.mb-mascot { display: block; width: 100%; height: 320px; position: relative; z-index: 1; }
|
| 186 |
+
@media (max-width: 780px) { .mb-mascot { height: 220px; } }
|
| 187 |
+
/* The pedestal: a class-coloured light pool under the figure β the character
|
| 188 |
+
stands ON something, which is most of what makes a select screen read as a
|
| 189 |
+
stage rather than a card. */
|
| 190 |
+
.mb-pedestal {
|
| 191 |
+
position: absolute; left: 50%; bottom: 46px; transform: translateX(-50%);
|
| 192 |
+
width: 78%; height: 64px; border-radius: 50%;
|
| 193 |
+
background: radial-gradient(ellipse at center,
|
| 194 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 34%, transparent) 0%,
|
| 195 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 10%, transparent) 55%,
|
| 196 |
+
transparent 72%);
|
| 197 |
+
filter: blur(2px);
|
| 198 |
+
}
|
| 199 |
+
/* Nameplate under the stage, class-coloured keyline. */
|
| 200 |
+
.mb-plate {
|
| 201 |
+
position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
|
| 202 |
+
text-align: center; padding: 8px 22px 10px; min-width: 60%;
|
| 203 |
+
border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, var(--border));
|
| 204 |
+
border-radius: 10px;
|
| 205 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 7%, var(--surface));
|
| 206 |
+
}
|
| 207 |
+
.mb-panel {
|
| 208 |
+
border: 1px solid var(--border); border-radius: 12px;
|
| 209 |
+
background: linear-gradient(180deg, var(--surface-2), transparent);
|
| 210 |
+
padding: 16px 18px 14px;
|
| 211 |
+
}
|
| 212 |
+
.mb-row-label {
|
| 213 |
+
font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
|
| 214 |
+
text-transform: uppercase; letter-spacing: 0.09em; margin: 10px 0 6px;
|
| 215 |
+
}
|
| 216 |
+
.mb-row-label:first-child { margin-top: 0; }
|
| 217 |
+
|
| 218 |
+
.mb-info { display: flex; flex-direction: column; gap: 14px; }
|
| 219 |
+
.mb-name { font-family: var(--display); font-size: var(--step-2); line-height: 1.1; }
|
| 220 |
+
.mb-params { font-family: var(--mono); font-size: var(--step--1); color: var(--dim); margin-top: 2px; }
|
| 221 |
+
|
| 222 |
+
.mb-variants { display: flex; gap: 6px; flex-wrap: wrap; }
|
| 223 |
+
/* Memory builds β the character screen's loadout row. Same pill grammar as
|
| 224 |
+
the quantisation picker: a build is a choice about THIS character, and the
|
| 225 |
+
selected one re-poses the figure (leaner, streamed experts drifting out). */
|
| 226 |
+
.mb-modes { display: flex; gap: 6px; flex-wrap: wrap; }
|
| 227 |
+
.mb-variant {
|
| 228 |
+
font-family: var(--mono); font-size: var(--step--1); cursor: pointer;
|
| 229 |
+
padding: 5px 12px; border-radius: 99px;
|
| 230 |
+
border: 1px solid var(--border-hi); background: transparent; color: var(--muted);
|
| 231 |
+
}
|
| 232 |
+
.mb-variant:hover { color: var(--text); border-color: var(--accent); }
|
| 233 |
+
.mb-variant[aria-selected="true"] { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }
|
| 234 |
+
|
| 235 |
+
.mb-stats { display: grid; grid-template-columns: repeat(3, auto); gap: 22px; margin: 14px 0 4px; justify-content: start; }
|
| 236 |
+
@media (max-width: 780px) { .mb-stats { justify-content: center; } }
|
| 237 |
+
/* At 390px the three-column stats row set a ~204px min-content floor which,
|
| 238 |
+
plus slide padding and the two 42px arrows, needed ~368px of a 342px
|
| 239 |
+
viewport β the body scrolled sideways. Flex-wrap has no min-content floor:
|
| 240 |
+
each stat drops to the next line instead of pushing the page. min-width: 0
|
| 241 |
+
on the grid/flex items is what lets the slide actually shrink. */
|
| 242 |
+
@media (max-width: 480px) {
|
| 243 |
+
.mb-stats { display: flex; flex-wrap: wrap; gap: 10px 20px; }
|
| 244 |
+
.mb-slide { min-width: 0; padding: 20px 18px; }
|
| 245 |
+
.mb-art, .mb-info { min-width: 0; }
|
| 246 |
+
.mb-arrow { width: 34px; height: 34px; font-size: 18px; }
|
| 247 |
+
}
|
| 248 |
+
.mb-stats dt {
|
| 249 |
+
font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
|
| 250 |
+
text-transform: uppercase; letter-spacing: 0.07em;
|
| 251 |
+
}
|
| 252 |
+
.mb-stats dd { margin: 4px 0 0; font-family: var(--mono); font-size: var(--step-0); color: var(--text); }
|
| 253 |
+
.mb-hw { color: var(--dim); font-size: var(--step--1); }
|
| 254 |
+
.mb-cached { color: var(--ok); font-size: var(--step--1); margin: 14px 0 0; }
|
| 255 |
+
.mb-ram { color: var(--warn); font-size: var(--step--1); margin: 14px 0 0; }
|
| 256 |
+
.mb-cta {
|
| 257 |
+
align-self: stretch; text-align: center; font-size: var(--step-0);
|
| 258 |
+
padding: 0.85rem 1.2rem;
|
| 259 |
+
background: var(--cs-accent, var(--accent));
|
| 260 |
+
border-color: var(--cs-accent, var(--accent));
|
| 261 |
+
}
|
| 262 |
+
.mb-cta:hover { filter: brightness(1.12); }
|
| 263 |
+
@media (max-width: 780px) { .mb-cta { align-self: center; } }
|
| 264 |
+
|
| 265 |
+
/* The roster rail β party lineup instead of dots. Each slot carries the
|
| 266 |
+
character's own snapshotted face (--thumb, painted async); cached models
|
| 267 |
+
were snapshotted lit, which is the visual for "unlocked". */
|
| 268 |
+
.mb-dots { display: flex; gap: 10px; justify-content: center; margin-top: 22px; flex-wrap: wrap; }
|
| 269 |
+
.mb-dot {
|
| 270 |
+
width: 64px; height: 76px; padding: 0 0 4px; cursor: pointer;
|
| 271 |
+
border: 1px solid var(--border-hi); border-radius: 10px;
|
| 272 |
+
background:
|
| 273 |
+
var(--thumb, radial-gradient(circle at 50% 40%, var(--border-hi) 0%, transparent 60%))
|
| 274 |
+
center 2px / 58px 58px no-repeat,
|
| 275 |
+
var(--surface);
|
| 276 |
+
display: flex; align-items: flex-end; justify-content: center;
|
| 277 |
+
transition: border-color 0.15s, transform 0.15s;
|
| 278 |
+
}
|
| 279 |
+
.mb-dot:hover { border-color: var(--muted); transform: translateY(-2px); }
|
| 280 |
+
.mb-dot[aria-selected="true"] {
|
| 281 |
+
border-color: var(--cs-accent, var(--accent));
|
| 282 |
+
box-shadow: 0 0 0 1px var(--cs-accent, var(--accent)),
|
| 283 |
+
0 6px 18px color-mix(in srgb, var(--cs-accent, var(--accent)) 25%, transparent);
|
| 284 |
+
}
|
| 285 |
+
.mb-dot-name {
|
| 286 |
+
font-family: var(--mono); font-size: 9px; color: var(--dim);
|
| 287 |
+
max-width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
| 288 |
+
}
|
| 289 |
+
.mb-dot[aria-selected="true"] .mb-dot-name { color: var(--text); }
|
| 290 |
+
@media (max-width: 480px) { .mb-dot { width: 52px; height: 64px; } .mb-dot-name { font-size: 8px; } }
|
| 291 |
+
.note { color: var(--muted); font-size: var(--step--1); margin-top: 16px; }
|
| 292 |
+
|
| 293 |
+
/* ββ kernel wall βββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 294 |
+
/* Five columns so the ten tiles fill exactly two rows β auto-fill left a
|
| 295 |
+
half-empty third row that read as a rendering fault. */
|
| 296 |
+
.wall { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px;
|
| 297 |
+
background: var(--border); border: 1px solid var(--border); border-radius: var(--rad);
|
| 298 |
+
overflow: hidden; }
|
| 299 |
+
@media (max-width: 900px) { .wall { grid-template-columns: repeat(3, 1fr); } }
|
| 300 |
+
@media (max-width: 560px) { .wall { grid-template-columns: repeat(2, 1fr); } }
|
| 301 |
+
.wall a {
|
| 302 |
+
background: var(--surface); padding: 16px 14px; color: var(--muted);
|
| 303 |
+
font-family: var(--mono); font-size: var(--step--1); display: block;
|
| 304 |
+
}
|
| 305 |
+
.wall a:hover { background: var(--surface-2); color: var(--accent); }
|
| 306 |
+
.wall b { display: block; color: var(--text); font-weight: 500; margin-bottom: 4px; }
|
| 307 |
+
.wall a:hover b { color: var(--accent); }
|
| 308 |
+
.wall span { color: var(--dim); }
|
| 309 |
+
|
| 310 |
+
/* ββ proof strip βββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 311 |
+
.proof {
|
| 312 |
+
border: 1px solid var(--border); border-left: 2px solid var(--accent);
|
| 313 |
+
border-radius: var(--rad); background: var(--surface); padding: 26px 28px;
|
| 314 |
+
}
|
| 315 |
+
.proof h2 { font-family: var(--display); font-weight: 400; font-size: var(--step-1); margin: 0; }
|
| 316 |
+
.proof p { color: var(--muted); margin: 10px 0 0; max-width: 70ch; }
|
| 317 |
+
|
| 318 |
+
/* ββ footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 319 |
+
footer { border-top: 1px solid var(--border); padding: 56px 0 72px; }
|
| 320 |
+
.foot-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 40px; }
|
| 321 |
+
@media (max-width: 780px) { .foot-grid { grid-template-columns: 1fr; gap: 28px; } }
|
| 322 |
+
footer h4 {
|
| 323 |
+
font-family: var(--mono); font-size: var(--step--1); text-transform: uppercase;
|
| 324 |
+
letter-spacing: 0.08em; color: var(--dim); margin: 0 0 12px; font-weight: 500;
|
| 325 |
+
}
|
| 326 |
+
footer ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
|
| 327 |
+
footer li a { color: var(--muted); font-size: var(--step--1); }
|
| 328 |
+
footer li a:hover { color: var(--accent); }
|
| 329 |
+
.foot-about { color: var(--muted); font-size: var(--step--1); max-width: 44ch; }
|
| 330 |
+
.foot-bottom {
|
| 331 |
+
margin-top: 44px; padding-top: 22px; border-top: 1px solid var(--border);
|
| 332 |
+
display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
|
| 333 |
+
font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
/* ββ THE GAME ENTRANCE βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 338 |
+
#models is the site: a full-viewport character select. The chrome is the
|
| 339 |
+
circuit-rune asset set in /entrance (bg painting, pedestal ring, corner
|
| 340 |
+
brackets, divider) β monochrome SVGs applied through CSS masks so every
|
| 341 |
+
ornament tints to the class accent of the model on stage. Data unchanged:
|
| 342 |
+
every value is the registry row. */
|
| 343 |
+
#models { padding: 0; }
|
| 344 |
+
#models .cs-root {
|
| 345 |
+
position: relative;
|
| 346 |
+
min-height: 100dvh;
|
| 347 |
+
display: grid;
|
| 348 |
+
grid-template-columns: minmax(270px, 350px) 1fr minmax(250px, 320px);
|
| 349 |
+
grid-template-rows: auto 1fr auto auto;
|
| 350 |
+
grid-template-areas:
|
| 351 |
+
'plate plate roster'
|
| 352 |
+
'sheet stage roster'
|
| 353 |
+
'enter enter roster'
|
| 354 |
+
'note note note';
|
| 355 |
+
gap: 10px 24px;
|
| 356 |
+
padding: calc(var(--nav-h, 64px) + 14px) 30px 24px;
|
| 357 |
+
background:
|
| 358 |
+
radial-gradient(ellipse at 50% 92%,
|
| 359 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 13%, transparent) 0%,
|
| 360 |
+
transparent 52%),
|
| 361 |
+
url('/entrance/bg.svg') center / cover no-repeat,
|
| 362 |
+
var(--bg);
|
| 363 |
+
overflow: hidden;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
/* Atmosphere: two fog bands drifting opposite ways, and a dust field. All
|
| 367 |
+
accent-neutral; the accent lives in the radial wash above. */
|
| 368 |
+
.cs-fog { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
|
| 369 |
+
.cs-fog i {
|
| 370 |
+
position: absolute; width: 90vw; height: 34vh; border-radius: 50%;
|
| 371 |
+
background: radial-gradient(ellipse at center, #18223a55 0%, transparent 65%);
|
| 372 |
+
filter: blur(30px);
|
| 373 |
+
}
|
| 374 |
+
.cs-fog i:nth-child(1) { left: -20vw; bottom: 4vh; animation: fogA 46s ease-in-out infinite alternate; }
|
| 375 |
+
.cs-fog i:nth-child(2) { right: -25vw; bottom: 12vh; animation: fogB 58s ease-in-out infinite alternate; }
|
| 376 |
+
@keyframes fogA { from { transform: translateX(0); } to { transform: translateX(9vw); } }
|
| 377 |
+
@keyframes fogB { from { transform: translateX(0); } to { transform: translateX(-11vw); } }
|
| 378 |
+
.cs-dust {
|
| 379 |
+
position: absolute; inset: -10% 0 0 0; pointer-events: none; z-index: 0; opacity: 0.5;
|
| 380 |
+
background-image:
|
| 381 |
+
radial-gradient(1.2px 1.2px at 12% 30%, #cfe0ff88 50%, transparent 51%),
|
| 382 |
+
radial-gradient(1px 1px at 34% 68%, #cfe0ff66 50%, transparent 51%),
|
| 383 |
+
radial-gradient(1.4px 1.4px at 58% 22%, #cfe0ff77 50%, transparent 51%),
|
| 384 |
+
radial-gradient(1px 1px at 71% 55%, #cfe0ff55 50%, transparent 51%),
|
| 385 |
+
radial-gradient(1.2px 1.2px at 86% 34%, #cfe0ff77 50%, transparent 51%),
|
| 386 |
+
radial-gradient(1px 1px at 45% 84%, #cfe0ff44 50%, transparent 51%);
|
| 387 |
+
background-size: 900px 700px;
|
| 388 |
+
animation: dust 90s linear infinite;
|
| 389 |
+
}
|
| 390 |
+
@keyframes dust { from { background-position: 0 0; } to { background-position: -900px 350px; } }
|
| 391 |
+
|
| 392 |
+
/* Nameplate: display face over an ornamental divider. */
|
| 393 |
+
#models .mb-plate {
|
| 394 |
+
grid-area: plate; position: relative; z-index: 1; justify-self: center;
|
| 395 |
+
text-align: center; margin-top: 4px; padding: 0 26px 14px;
|
| 396 |
+
border: 0; background: none;
|
| 397 |
+
}
|
| 398 |
+
#models .mb-name {
|
| 399 |
+
font-family: var(--display); font-size: clamp(1.9rem, 3.6vw, 2.9rem);
|
| 400 |
+
line-height: 1.05; text-shadow: 0 2px 24px #000c;
|
| 401 |
+
}
|
| 402 |
+
#models .mb-params {
|
| 403 |
+
font-family: var(--mono); font-size: var(--step--1); letter-spacing: 0.14em;
|
| 404 |
+
text-transform: uppercase; margin-top: 6px;
|
| 405 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 75%, var(--text));
|
| 406 |
+
}
|
| 407 |
+
#models .mb-plate::after {
|
| 408 |
+
content: ''; display: block; height: 16px; margin: 10px auto 0; width: min(360px, 80%);
|
| 409 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff);
|
| 410 |
+
-webkit-mask: url('/entrance/divider.svg') center / contain no-repeat;
|
| 411 |
+
mask: url('/entrance/divider.svg') center / contain no-repeat;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* The stage. */
|
| 415 |
+
#models .mb-stage { grid-area: stage; display: flex; align-items: stretch; gap: 4px; min-height: 0; position: relative; z-index: 1; }
|
| 416 |
+
#models .mb-art { flex: 1; padding: 0; display: flex; align-items: center; justify-content: center; position: relative; }
|
| 417 |
+
#models .mb-mascot { height: min(54dvh, 500px); max-width: 100%; position: relative; z-index: 2; }
|
| 418 |
+
#models .mb-pedestal {
|
| 419 |
+
position: absolute; left: 50%; bottom: 2%; transform: translateX(-50%);
|
| 420 |
+
width: min(560px, 86%); height: 150px; border-radius: 0; filter: none;
|
| 421 |
+
background: radial-gradient(ellipse at center,
|
| 422 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 30%, transparent) 0%,
|
| 423 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 8%, transparent) 52%,
|
| 424 |
+
transparent 70%);
|
| 425 |
+
}
|
| 426 |
+
#models .mb-pedestal::after {
|
| 427 |
+
content: ''; position: absolute; inset: 0;
|
| 428 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
|
| 429 |
+
-webkit-mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
|
| 430 |
+
mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
|
| 431 |
+
animation: ringPulse 5s ease-in-out infinite;
|
| 432 |
+
}
|
| 433 |
+
@keyframes ringPulse { 0%, 100% { opacity: 0.85; } 50% { opacity: 0.5; } }
|
| 434 |
+
#models .mb-arrow {
|
| 435 |
+
align-self: center; width: 52px; height: 76px; border-radius: 12px;
|
| 436 |
+
background: transparent; border: 1px solid transparent; font-size: 34px;
|
| 437 |
+
color: var(--dim); z-index: 2;
|
| 438 |
+
}
|
| 439 |
+
#models .mb-arrow:hover { color: var(--cs-accent, var(--accent)); border-color: var(--border); background: #0b101cbb; }
|
| 440 |
+
|
| 441 |
+
/* The sheet: glass plate with rune corners. */
|
| 442 |
+
#models .mb-info { grid-area: sheet; align-self: center; z-index: 1; }
|
| 443 |
+
#models .mb-panel {
|
| 444 |
+
position: relative; border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 22%, var(--border));
|
| 445 |
+
border-radius: 4px; padding: 20px 20px 16px;
|
| 446 |
+
background: linear-gradient(180deg, #0c1220ee, #090e18e6);
|
| 447 |
+
box-shadow: inset 0 1px 0 #ffffff0a, 0 18px 50px #0009;
|
| 448 |
+
backdrop-filter: blur(4px);
|
| 449 |
+
}
|
| 450 |
+
#models .mb-panel::before, #models .mb-panel::after {
|
| 451 |
+
content: ''; position: absolute; width: 40px; height: 40px;
|
| 452 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
|
| 453 |
+
-webkit-mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
|
| 454 |
+
mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
|
| 455 |
+
}
|
| 456 |
+
#models .mb-panel::before { top: -5px; left: -5px; }
|
| 457 |
+
#models .mb-panel::after { bottom: -5px; right: -5px; transform: rotate(180deg); }
|
| 458 |
+
#models .mb-row-label {
|
| 459 |
+
font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.16em;
|
| 460 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, var(--dim));
|
| 461 |
+
text-transform: uppercase; margin: 14px 0 7px;
|
| 462 |
+
}
|
| 463 |
+
#models .mb-row-label:first-child { margin-top: 0; }
|
| 464 |
+
#models .mb-stats { display: block; margin: 12px 0 2px; }
|
| 465 |
+
#models .mb-stats > div {
|
| 466 |
+
display: flex; justify-content: space-between; align-items: baseline;
|
| 467 |
+
padding: 6px 2px; border-bottom: 1px solid #ffffff0d;
|
| 468 |
+
}
|
| 469 |
+
#models .mb-stats dt {
|
| 470 |
+
font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.12em; color: var(--dim);
|
| 471 |
+
}
|
| 472 |
+
#models .mb-stats dd {
|
| 473 |
+
margin: 0; font-family: var(--mono); font-size: var(--step-0);
|
| 474 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, var(--text));
|
| 475 |
+
}
|
| 476 |
+
#models .mb-variant {
|
| 477 |
+
border-radius: 3px; border-color: #ffffff1c; background: #ffffff07;
|
| 478 |
+
}
|
| 479 |
+
#models .mb-variant[aria-selected="true"] {
|
| 480 |
+
border-color: var(--cs-accent, var(--accent));
|
| 481 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 18%, transparent);
|
| 482 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, #fff);
|
| 483 |
+
}
|
| 484 |
+
#models .mb-cached { color: var(--ok); font-size: var(--step--1); margin: 10px 0 0; }
|
| 485 |
+
#models .mb-ram { font-size: var(--step--1); margin: 8px 0 0; }
|
| 486 |
+
|
| 487 |
+
/* Roster rail. */
|
| 488 |
+
#models .cs-roster-head {
|
| 489 |
+
grid-area: roster; align-self: start; z-index: 1;
|
| 490 |
+
font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.18em;
|
| 491 |
+
text-transform: uppercase; color: var(--dim);
|
| 492 |
+
padding: 2px 0 0 16px; height: 20px;
|
| 493 |
+
}
|
| 494 |
+
#models .mb-roster {
|
| 495 |
+
grid-area: roster; z-index: 1; margin: 26px 0 0; padding: 0 4px 4px 14px;
|
| 496 |
+
display: flex; flex-direction: column; gap: 8px; justify-content: flex-start;
|
| 497 |
+
overflow-y: auto; border-left: 1px solid #ffffff12;
|
| 498 |
+
scrollbar-width: thin; scrollbar-color: var(--border-hi) transparent;
|
| 499 |
+
}
|
| 500 |
+
#models .mb-dot {
|
| 501 |
+
width: 100%; height: auto; min-height: 64px; padding: 7px 10px;
|
| 502 |
+
display: flex; flex-direction: row; align-items: center; gap: 11px;
|
| 503 |
+
text-align: left; border-radius: 4px; cursor: pointer;
|
| 504 |
+
border: 1px solid #ffffff14;
|
| 505 |
+
background: linear-gradient(180deg, #ffffff08, transparent 60%), #0a0f1acc;
|
| 506 |
+
transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
|
| 507 |
+
position: relative;
|
| 508 |
+
}
|
| 509 |
+
#models .mb-dot:hover { border-color: var(--muted); transform: translateX(-2px); }
|
| 510 |
+
#models .mb-dot[aria-selected="true"] {
|
| 511 |
+
border-color: var(--cs-accent, var(--accent));
|
| 512 |
+
box-shadow: inset 0 0 0 1px var(--cs-accent, var(--accent)),
|
| 513 |
+
0 0 22px color-mix(in srgb, var(--cs-accent, var(--accent)) 30%, transparent);
|
| 514 |
+
}
|
| 515 |
+
#models .mb-dot[data-cached]::after {
|
| 516 |
+
content: 'READY'; position: absolute; top: 5px; right: 7px;
|
| 517 |
+
font-family: var(--mono); font-size: 0.5rem; letter-spacing: 0.12em;
|
| 518 |
+
color: var(--ok); border: 1px solid color-mix(in srgb, var(--ok) 55%, transparent);
|
| 519 |
+
border-radius: 3px; padding: 1px 4px;
|
| 520 |
+
}
|
| 521 |
+
.mb-dot-face {
|
| 522 |
+
flex: 0 0 46px; height: 46px; border-radius: 4px;
|
| 523 |
+
background: var(--thumb, radial-gradient(circle at 50% 40%, var(--border-hi) 0%, transparent 60%))
|
| 524 |
+
center / cover no-repeat, #0d1322;
|
| 525 |
+
border: 1px solid #ffffff10;
|
| 526 |
+
}
|
| 527 |
+
.mb-dot-text { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
|
| 528 |
+
.mb-dot-text b { font-size: 0.8rem; font-weight: 600; color: var(--text);
|
| 529 |
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 530 |
+
.mb-dot-text i { font-style: normal; font-family: var(--mono); font-size: 0.6rem;
|
| 531 |
+
color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 532 |
+
|
| 533 |
+
/* ENTER. */
|
| 534 |
+
#models .cs-enter { grid-area: enter; display: flex; justify-content: center; padding: 4px 0 2px; z-index: 1; }
|
| 535 |
+
#models .cs-enter .mb-cta {
|
| 536 |
+
min-width: 340px; text-align: center; letter-spacing: 0.14em; text-transform: uppercase;
|
| 537 |
+
font-weight: 700; font-size: var(--step-0); padding: 0.95rem 1.4rem;
|
| 538 |
+
border-radius: 4px; position: relative; overflow: hidden;
|
| 539 |
+
color: #0b0d10;
|
| 540 |
+
background: linear-gradient(180deg,
|
| 541 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff) 0%,
|
| 542 |
+
var(--cs-accent, var(--accent)) 55%,
|
| 543 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, #000) 100%);
|
| 544 |
+
border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, #fff);
|
| 545 |
+
box-shadow: 0 10px 34px color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, transparent),
|
| 546 |
+
inset 0 1px 0 #ffffff66;
|
| 547 |
+
}
|
| 548 |
+
#models .cs-enter .mb-cta:hover { filter: brightness(1.1); }
|
| 549 |
+
#models .cs-enter .mb-cta::after {
|
| 550 |
+
content: ''; position: absolute; top: 0; bottom: 0; width: 40%;
|
| 551 |
+
left: -60%; transform: skewX(-20deg);
|
| 552 |
+
background: linear-gradient(90deg, transparent, #ffffff55, transparent);
|
| 553 |
+
transition: left 0.5s ease;
|
| 554 |
+
}
|
| 555 |
+
#models .cs-enter .mb-cta:hover::after { left: 130%; }
|
| 556 |
+
#models .cs-note { grid-area: note; text-align: center; z-index: 1; }
|
| 557 |
+
|
| 558 |
+
/* The logo opening. */
|
| 559 |
+
.cs-splash {
|
| 560 |
+
position: absolute; inset: 0; z-index: 30;
|
| 561 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;
|
| 562 |
+
background: radial-gradient(ellipse at 50% 42%, #0d1424 0%, var(--bg) 70%);
|
| 563 |
+
animation: csSplashOut 2.6s ease forwards;
|
| 564 |
+
}
|
| 565 |
+
.cs-emblem {
|
| 566 |
+
width: 84px; height: 84px; color: var(--accent);
|
| 567 |
+
animation: csEmblem 1.4s cubic-bezier(0.16, 1, 0.3, 1) both;
|
| 568 |
+
}
|
| 569 |
+
.cs-logo {
|
| 570 |
+
font-family: var(--display); font-size: clamp(3rem, 9vw, 6.5rem); color: var(--text);
|
| 571 |
+
animation: csLogoIn 1.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
| 572 |
+
}
|
| 573 |
+
.cs-logo b { color: var(--accent); }
|
| 574 |
+
.cs-sub {
|
| 575 |
+
font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.22em;
|
| 576 |
+
text-transform: uppercase; color: var(--dim);
|
| 577 |
+
animation: csSub 1s ease 0.7s both;
|
| 578 |
+
}
|
| 579 |
+
@keyframes csEmblem {
|
| 580 |
+
0% { opacity: 0; transform: rotate(-90deg) scale(0.6); }
|
| 581 |
+
100% { opacity: 1; transform: rotate(0deg) scale(1); }
|
| 582 |
+
}
|
| 583 |
+
@keyframes csLogoIn {
|
| 584 |
+
0% { opacity: 0; transform: scale(1.22); letter-spacing: 0.16em; filter: blur(12px); }
|
| 585 |
+
55% { opacity: 1; filter: blur(0); }
|
| 586 |
+
100% { opacity: 1; transform: scale(1); letter-spacing: 0.01em; }
|
| 587 |
+
}
|
| 588 |
+
@keyframes csSub { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
| 589 |
+
@keyframes csSplashOut { 0%, 78% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
|
| 590 |
+
|
| 591 |
+
@media (prefers-reduced-motion: reduce) {
|
| 592 |
+
.cs-fog i, .cs-dust, #models .mb-pedestal::after { animation: none; }
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
/* Phones: stage first, roster as a strip, sheet, enter. */
|
| 596 |
+
@media (max-width: 900px) {
|
| 597 |
+
#models .cs-root {
|
| 598 |
+
grid-template-columns: 1fr;
|
| 599 |
+
grid-template-areas: 'plate' 'stage' 'roster' 'sheet' 'enter' 'note';
|
| 600 |
+
padding: calc(var(--nav-h, 64px) + 8px) 14px 18px;
|
| 601 |
+
gap: 8px;
|
| 602 |
+
}
|
| 603 |
+
#models .mb-mascot { height: min(36dvh, 300px); }
|
| 604 |
+
#models .cs-roster-head { display: none; }
|
| 605 |
+
#models .mb-roster {
|
| 606 |
+
flex-direction: row; overflow-x: auto; overflow-y: hidden; margin-top: 0;
|
| 607 |
+
border-left: 0; border-top: 1px solid #ffffff12; padding: 10px 2px 4px;
|
| 608 |
+
}
|
| 609 |
+
#models .mb-dot { width: auto; min-width: 168px; flex: 0 0 auto; }
|
| 610 |
+
#models .cs-enter .mb-cta { min-width: 0; width: 100%; }
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
/* ββ ROUND TWO: depth, transitions, identity βββββββββββββββββββββββββββββββ */
|
| 615 |
+
|
| 616 |
+
/* The floor reflection β Chromium's box-reflect, which is the browser class
|
| 617 |
+
the engine requires anyway. Elsewhere it simply does not draw. */
|
| 618 |
+
#models .mb-mascot {
|
| 619 |
+
-webkit-box-reflect: below -6px linear-gradient(transparent 62%, rgba(255,255,255,0.10));
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
/* Selection transition. go() re-arms .cs-in, so switching characters flashes
|
| 623 |
+
the pedestal and walks the plate and sheet back in β selection FEELS like
|
| 624 |
+
selection. Names are transform/opacity only; nothing relayouts. */
|
| 625 |
+
#models .mb-art.cs-in .mb-pedestal::after { animation: ringFlash 0.7s ease-out, ringPulse 5s ease-in-out 0.7s infinite; }
|
| 626 |
+
@keyframes ringFlash {
|
| 627 |
+
0% { opacity: 1; transform: scale(0.92); filter: brightness(2.2); }
|
| 628 |
+
100% { opacity: 0.85; transform: scale(1); filter: brightness(1); }
|
| 629 |
+
}
|
| 630 |
+
#models .mb-art.cs-in .mb-mascot { animation: charIn 0.5s cubic-bezier(0.2, 1.4, 0.4, 1); }
|
| 631 |
+
@keyframes charIn {
|
| 632 |
+
0% { opacity: 0; transform: translateY(14px) scale(0.965); }
|
| 633 |
+
100% { opacity: 1; transform: translateY(0) scale(1); }
|
| 634 |
+
}
|
| 635 |
+
#models .mb-plate.cs-in .mb-name { animation: plateIn 0.45s ease both; }
|
| 636 |
+
#models .mb-plate.cs-in .mb-params { animation: plateIn 0.45s ease 0.08s both; }
|
| 637 |
+
@keyframes plateIn {
|
| 638 |
+
0% { opacity: 0; transform: translateY(-8px); letter-spacing: 0.08em; }
|
| 639 |
+
100% { opacity: 1; transform: translateY(0); }
|
| 640 |
+
}
|
| 641 |
+
#models .mb-panel.cs-in > * { animation: rowIn 0.4s ease both; }
|
| 642 |
+
#models .mb-panel.cs-in > *:nth-child(2) { animation-delay: 0.05s; }
|
| 643 |
+
#models .mb-panel.cs-in > *:nth-child(3) { animation-delay: 0.10s; }
|
| 644 |
+
#models .mb-panel.cs-in > *:nth-child(4) { animation-delay: 0.15s; }
|
| 645 |
+
#models .mb-panel.cs-in > *:nth-child(5) { animation-delay: 0.20s; }
|
| 646 |
+
#models .mb-panel.cs-in > *:nth-child(n+6) { animation-delay: 0.25s; }
|
| 647 |
+
@keyframes rowIn {
|
| 648 |
+
0% { opacity: 0; transform: translateX(-10px); }
|
| 649 |
+
100% { opacity: 1; transform: translateX(0); }
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
/* Class sigil beside the class line. */
|
| 653 |
+
#models .mb-params { display: inline-flex; align-items: center; gap: 8px; justify-content: center; }
|
| 654 |
+
.mb-sigil { display: inline-flex; width: 16px; height: 16px; }
|
| 655 |
+
.mb-sigil svg { width: 100%; height: 100%; }
|
| 656 |
+
|
| 657 |
+
/* Realm corners: tiny mono statements, both literally true. */
|
| 658 |
+
.cs-corner {
|
| 659 |
+
position: absolute; bottom: 10px; z-index: 1;
|
| 660 |
+
font-family: var(--mono); font-size: 0.58rem; letter-spacing: 0.14em;
|
| 661 |
+
text-transform: uppercase; color: color-mix(in srgb, var(--dim) 70%, transparent);
|
| 662 |
+
pointer-events: none;
|
| 663 |
+
}
|
| 664 |
+
.cs-corner-l { left: 16px; }
|
| 665 |
+
.cs-corner-r { right: 16px; }
|
| 666 |
+
|
| 667 |
+
/* READY glint: a light pass over the tag every few seconds β unlocked should
|
| 668 |
+
glimmer. Masked to the tag only. */
|
| 669 |
+
#models .mb-dot[data-cached]::after { overflow: hidden; }
|
| 670 |
+
#models .mb-dot[data-cached] { isolation: isolate; }
|
| 671 |
+
#models .mb-dot[data-cached]::before {
|
| 672 |
+
content: ''; position: absolute; top: 3px; right: 5px; width: 44px; height: 14px;
|
| 673 |
+
background: linear-gradient(110deg, transparent 30%, #ffffff2e 50%, transparent 70%) no-repeat;
|
| 674 |
+
background-size: 200% 100%;
|
| 675 |
+
animation: readyGlint 4.5s ease-in-out infinite;
|
| 676 |
+
pointer-events: none; z-index: 1;
|
| 677 |
+
}
|
| 678 |
+
@keyframes readyGlint {
|
| 679 |
+
0%, 72% { background-position: 180% 0; }
|
| 680 |
+
92%, 100% { background-position: -80% 0; }
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
/* Enter: charge on hover, visible keyboard focus. */
|
| 684 |
+
#models .cs-enter .mb-cta { transition: transform 0.18s ease, filter 0.18s ease, box-shadow 0.18s ease; }
|
| 685 |
+
#models .cs-enter .mb-cta:hover {
|
| 686 |
+
transform: translateY(-1px) scale(1.015);
|
| 687 |
+
box-shadow: 0 14px 44px color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, transparent),
|
| 688 |
+
inset 0 1px 0 #ffffff77;
|
| 689 |
+
}
|
| 690 |
+
#models .cs-enter .mb-cta:focus-visible {
|
| 691 |
+
outline: 2px solid #fff; outline-offset: 3px;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
/* Parallax layers move on transform β give them will-change and keep the
|
| 695 |
+
fog's own drift composed with the pointer offset via nesting (the <i>
|
| 696 |
+
children animate, the container translates). */
|
| 697 |
+
.cs-fog, .cs-dust, #models .mb-art { will-change: transform; transition: transform 0.3s ease-out; }
|
| 698 |
+
|
| 699 |
+
@media (prefers-reduced-motion: reduce) {
|
| 700 |
+
#models .mb-art.cs-in .mb-mascot,
|
| 701 |
+
#models .mb-plate.cs-in .mb-name, #models .mb-plate.cs-in .mb-params,
|
| 702 |
+
#models .mb-panel.cs-in > *, #models .mb-dot[data-cached]::before { animation: none; }
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
/* ββ ROUNDS 3-6: the aura, the juice, the frame ββββββββββββββββββββββββββββ */
|
| 707 |
+
|
| 708 |
+
/* The aura: motes of the class light rising off the pedestal, their pace set
|
| 709 |
+
by the model's measured rate (--cs-tempo β the same number the mascot's
|
| 710 |
+
idle clock reads). A fast little model boils; a 35B smoulders. */
|
| 711 |
+
.cs-aura { position: absolute; left: 50%; bottom: 8%; width: min(480px, 70%); height: 60%;
|
| 712 |
+
transform: translateX(-50%); pointer-events: none; z-index: 1; }
|
| 713 |
+
.cs-aura i {
|
| 714 |
+
position: absolute; bottom: 0; width: 4px; height: 4px; border-radius: 50%;
|
| 715 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
|
| 716 |
+
opacity: 0;
|
| 717 |
+
animation: mote calc(5.2s / var(--cs-tempo, 1)) ease-in infinite;
|
| 718 |
+
}
|
| 719 |
+
.cs-aura i:nth-child(1) { left: 6%; animation-delay: 0.0s; }
|
| 720 |
+
.cs-aura i:nth-child(2) { left: 16%; animation-delay: 1.1s; width: 3px; height: 3px; }
|
| 721 |
+
.cs-aura i:nth-child(3) { left: 27%; animation-delay: 2.3s; }
|
| 722 |
+
.cs-aura i:nth-child(4) { left: 38%; animation-delay: 0.6s; width: 5px; height: 5px; }
|
| 723 |
+
.cs-aura i:nth-child(5) { left: 50%; animation-delay: 1.7s; width: 3px; height: 3px; }
|
| 724 |
+
.cs-aura i:nth-child(6) { left: 61%; animation-delay: 2.9s; }
|
| 725 |
+
.cs-aura i:nth-child(7) { left: 70%; animation-delay: 0.3s; }
|
| 726 |
+
.cs-aura i:nth-child(8) { left: 80%; animation-delay: 1.9s; width: 3px; height: 3px; }
|
| 727 |
+
.cs-aura i:nth-child(9) { left: 89%; animation-delay: 2.6s; }
|
| 728 |
+
.cs-aura i:nth-child(10) { left: 95%; animation-delay: 1.3s; width: 5px; height: 5px; }
|
| 729 |
+
@keyframes mote {
|
| 730 |
+
0% { opacity: 0; transform: translateY(0) scale(1); }
|
| 731 |
+
12% { opacity: 0.9; }
|
| 732 |
+
100% { opacity: 0; transform: translateY(-46vh) scale(0.4); }
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
/* ARMED: a cached character's circle runs a rotating highlight β the same
|
| 736 |
+
OPFS truth as the READY tag, drawn as ceremony. Conic light clipped by the
|
| 737 |
+
very ring mask. */
|
| 738 |
+
#models .mb-art[data-armed] .mb-pedestal::before {
|
| 739 |
+
content: ''; position: absolute; inset: 0;
|
| 740 |
+
background: conic-gradient(from 0deg,
|
| 741 |
+
transparent 0deg, transparent 290deg,
|
| 742 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 90%, #fff) 330deg,
|
| 743 |
+
transparent 360deg);
|
| 744 |
+
-webkit-mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
|
| 745 |
+
mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
|
| 746 |
+
animation: armedSpin 6s linear infinite;
|
| 747 |
+
}
|
| 748 |
+
@keyframes armedSpin { to { transform: rotate(360deg); } }
|
| 749 |
+
|
| 750 |
+
/* Lore under the nameplate: computed from the spec, styled as inscription. */
|
| 751 |
+
.cs-lore {
|
| 752 |
+
margin-top: 8px; font-size: 0.8rem; font-style: italic;
|
| 753 |
+
color: color-mix(in srgb, var(--text) 62%, transparent);
|
| 754 |
+
letter-spacing: 0.02em;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
/* Juice: chips stamp when chosen, roster cards press, ENTER presses. */
|
| 758 |
+
#models .mb-variant[aria-selected="true"] { animation: stamp 0.22s ease; }
|
| 759 |
+
@keyframes stamp { 0% { transform: scale(1.08); } 100% { transform: scale(1); } }
|
| 760 |
+
#models .mb-dot:active { transform: translateX(-2px) scale(0.98); }
|
| 761 |
+
#models .cs-enter .mb-cta:active { transform: translateY(1px) scale(0.995); box-shadow: 0 6px 20px color-mix(in srgb, var(--cs-accent, var(--accent)) 35%, transparent); }
|
| 762 |
+
|
| 763 |
+
/* The cinematic frame: a breath of dark at the top and bottom edges, so the
|
| 764 |
+
scene reads composed rather than cropped. */
|
| 765 |
+
#models .cs-root::before, #models .cs-root::after {
|
| 766 |
+
content: ''; position: absolute; left: 0; right: 0; height: 90px;
|
| 767 |
+
pointer-events: none; z-index: 2;
|
| 768 |
+
}
|
| 769 |
+
#models .cs-root::before { top: 0; background: linear-gradient(180deg, #05070dcc, transparent); }
|
| 770 |
+
#models .cs-root::after { bottom: 0; background: linear-gradient(0deg, #05070d99, transparent); }
|
| 771 |
+
|
| 772 |
+
/* Screen-reader announcer. */
|
| 773 |
+
.cs-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
| 774 |
+
|
| 775 |
+
@media (prefers-reduced-motion: reduce) {
|
| 776 |
+
.cs-aura i, #models .mb-art[data-armed] .mb-pedestal::before,
|
| 777 |
+
#models .mb-variant[aria-selected="true"] { animation: none; }
|
| 778 |
+
.cs-aura i { opacity: 0.35; transform: translateY(-8vh); }
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
/* ββ ROUND 7: THE ENVIRONMENT ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 783 |
+
The realm gains architecture. Silhouette layers between backdrop and
|
| 784 |
+
stage, each on its own parallax rate: obelisk skyline far, rune columns
|
| 785 |
+
near, the character floating between them under an aerial ring. */
|
| 786 |
+
.cs-spires {
|
| 787 |
+
position: absolute; left: -3%; right: -3%; bottom: 0; height: 42vh;
|
| 788 |
+
background: url('/entrance/spires.svg') bottom center / cover no-repeat;
|
| 789 |
+
opacity: 0.85; pointer-events: none; z-index: 0;
|
| 790 |
+
}
|
| 791 |
+
.cs-col {
|
| 792 |
+
position: absolute; bottom: -2vh; width: min(240px, 16vw); height: 92vh;
|
| 793 |
+
background: url('/entrance/column.svg') bottom / contain no-repeat;
|
| 794 |
+
pointer-events: none; z-index: 1; opacity: 0.9;
|
| 795 |
+
}
|
| 796 |
+
.cs-col-l { left: max(6px, 2vw); }
|
| 797 |
+
.cs-col-r { right: max(6px, 2vw); transform: scaleX(-1); }
|
| 798 |
+
|
| 799 |
+
/* The aerial ring: a great slow wheel behind the character, in the class
|
| 800 |
+
light at whisper opacity. The armed floor ring is ceremony; this is
|
| 801 |
+
architecture. */
|
| 802 |
+
.cs-backring {
|
| 803 |
+
position: absolute; left: 50%; top: 46%; width: min(64dvh, 620px); aspect-ratio: 1;
|
| 804 |
+
translate: -50% -50%;
|
| 805 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, #fff);
|
| 806 |
+
-webkit-mask: url('/entrance/backring.svg') center / contain no-repeat;
|
| 807 |
+
mask: url('/entrance/backring.svg') center / contain no-repeat;
|
| 808 |
+
opacity: 0.14; z-index: 0;
|
| 809 |
+
animation: backringSpin 80s linear infinite;
|
| 810 |
+
}
|
| 811 |
+
@keyframes backringSpin { to { rotate: 360deg; } }
|
| 812 |
+
|
| 813 |
+
/* Nameplate banner wings behind the name. */
|
| 814 |
+
#models .mb-plate { padding-top: 10px; }
|
| 815 |
+
.cs-banner {
|
| 816 |
+
position: absolute; left: 50%; top: 16px; transform: translateX(-50%);
|
| 817 |
+
width: min(640px, 92vw); height: 80px; pointer-events: none;
|
| 818 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 65%, #fff);
|
| 819 |
+
-webkit-mask: url('/entrance/banner.svg') center / contain no-repeat;
|
| 820 |
+
mask: url('/entrance/banner.svg') center / contain no-repeat;
|
| 821 |
+
opacity: 0.5; z-index: -1;
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
/* Portrait frames on the roster faces. */
|
| 825 |
+
.mb-dot-face { position: relative; }
|
| 826 |
+
.mb-dot-face::after {
|
| 827 |
+
content: ''; position: absolute; inset: -3px;
|
| 828 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, #9fb4d8);
|
| 829 |
+
-webkit-mask: url('/entrance/pframe.svg') center / contain no-repeat;
|
| 830 |
+
mask: url('/entrance/pframe.svg') center / contain no-repeat;
|
| 831 |
+
opacity: 0.35; transition: opacity 0.15s;
|
| 832 |
+
}
|
| 833 |
+
#models .mb-dot[aria-selected="true"] .mb-dot-face::after,
|
| 834 |
+
#models .mb-dot:hover .mb-dot-face::after { opacity: 0.9; }
|
| 835 |
+
|
| 836 |
+
/* The cinematic edges upgraded: the dark breath now carries the rune border
|
| 837 |
+
strip along its inner line. */
|
| 838 |
+
#models .cs-root::before {
|
| 839 |
+
background:
|
| 840 |
+
linear-gradient(180deg, #05070dcc, transparent),
|
| 841 |
+
linear-gradient(180deg, transparent 76px, transparent 76px);
|
| 842 |
+
}
|
| 843 |
+
#models .cs-root > .cs-edge-t, #models .cs-root > .cs-edge-b { display: none; }
|
| 844 |
+
#models .cs-root::after {
|
| 845 |
+
background: linear-gradient(0deg, #05070d99, transparent);
|
| 846 |
+
}
|
| 847 |
+
.cs-borderline-t, .cs-borderline-b {
|
| 848 |
+
position: absolute; left: 8%; right: 8%; height: 12px; z-index: 2; pointer-events: none;
|
| 849 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, #6c7f9f);
|
| 850 |
+
-webkit-mask: url('/entrance/border.svg') center / auto 100% repeat-x;
|
| 851 |
+
mask: url('/entrance/border.svg') center / auto 100% repeat-x;
|
| 852 |
+
opacity: 0.4;
|
| 853 |
+
}
|
| 854 |
+
.cs-borderline-t { top: calc(var(--nav-h, 64px) + 2px); }
|
| 855 |
+
.cs-borderline-b { bottom: 8px; }
|
| 856 |
+
|
| 857 |
+
/* The splash's ring burst: the aerial ring blooming open behind the emblem. */
|
| 858 |
+
.cs-splash { position: absolute; }
|
| 859 |
+
.cs-splash-ring {
|
| 860 |
+
position: absolute; width: 420px; aspect-ratio: 1;
|
| 861 |
+
background: var(--accent);
|
| 862 |
+
-webkit-mask: url('/entrance/backring.svg') center / contain no-repeat;
|
| 863 |
+
mask: url('/entrance/backring.svg') center / contain no-repeat;
|
| 864 |
+
animation: splashRing 2.2s cubic-bezier(0.16, 1, 0.3, 1) both;
|
| 865 |
+
opacity: 0.22;
|
| 866 |
+
}
|
| 867 |
+
@keyframes splashRing {
|
| 868 |
+
0% { transform: scale(0.4) rotate(-40deg); opacity: 0; }
|
| 869 |
+
40% { opacity: 0.3; }
|
| 870 |
+
100% { transform: scale(1.05) rotate(8deg); opacity: 0.18; }
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
/* The realm's cursor: a small trace-diamond. Entrance only. */
|
| 874 |
+
#models .cs-root, #models .cs-root a, #models .cs-root button {
|
| 875 |
+
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22"><path d="M11 1 L21 11 L11 21 L1 11 Z" fill="none" stroke="white" stroke-width="2"/><path d="M11 7 L15 11 L11 15 L7 11 Z" fill="white"/></svg>') 11 11, auto;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
@media (prefers-reduced-motion: reduce) {
|
| 879 |
+
.cs-backring, .cs-splash-ring { animation: none; }
|
| 880 |
+
}
|
| 881 |
+
@media (max-width: 900px) {
|
| 882 |
+
.cs-col { display: none; }
|
| 883 |
+
.cs-spires { height: 26vh; opacity: 0.6; }
|
| 884 |
+
.cs-backring { width: min(78vw, 380px); top: 40%; }
|
| 885 |
+
.cs-banner { height: 56px; opacity: 0.4; }
|
| 886 |
+
.cs-borderline-t, .cs-borderline-b { display: none; }
|
| 887 |
+
}
|
| 888 |
+
|
| 889 |
+
|
| 890 |
+
/* ββ ROUND 8: THE ALIGNMENT PASS βββββββββββββββββββββββββββββββββββββββββββ
|
| 891 |
+
One rhythm: an 8px gutter system, a hard max-width so ultrawide screens
|
| 892 |
+
frame the scene instead of stretching it, the sheet and roster hung from
|
| 893 |
+
the same top line, and the enter slab locked to the stage's centre line. */
|
| 894 |
+
#models .cs-root {
|
| 895 |
+
--cs-gutter: 24px;
|
| 896 |
+
max-width: 1720px; margin: 0 auto;
|
| 897 |
+
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr) minmax(260px, 310px);
|
| 898 |
+
gap: 8px var(--cs-gutter);
|
| 899 |
+
padding: calc(var(--nav-h, 64px) + 12px) max(28px, 4vw) 22px;
|
| 900 |
+
}
|
| 901 |
+
/* Sheet and roster share a top line: both start where the stage starts. */
|
| 902 |
+
#models .mb-info { align-self: start; padding-top: 34px; }
|
| 903 |
+
#models .cs-roster-head { padding-top: 8px; height: 26px; }
|
| 904 |
+
#models .mb-roster { margin-top: 34px; }
|
| 905 |
+
/* The plate is the one centred element; give it a fixed rhythm below. */
|
| 906 |
+
#models .mb-plate { margin: 2px 0 0; padding-bottom: 6px; }
|
| 907 |
+
/* Enter hangs centred under the stage column only, not the whole row β
|
| 908 |
+
optically aligned with the character above it. */
|
| 909 |
+
#models .cs-enter { grid-column: 2; justify-self: center; }
|
| 910 |
+
#models .cs-corner-l { left: max(28px, 4vw); }
|
| 911 |
+
#models .cs-corner-r { right: max(28px, 4vw); }
|
| 912 |
+
/* Border strips share the content inset. */
|
| 913 |
+
.cs-borderline-t, .cs-borderline-b { left: max(28px, 4vw); right: max(28px, 4vw); }
|
| 914 |
+
/* Stat rows: value column right-aligned on a shared edge, tabular digits. */
|
| 915 |
+
#models .mb-stats dd { font-variant-numeric: tabular-nums; text-align: right; }
|
| 916 |
+
#models .mb-stats dt { padding-right: 12px; }
|
| 917 |
+
/* Chips wrap on the same 8px rhythm. */
|
| 918 |
+
#models .mb-variants, #models .mb-modes { gap: 8px; }
|
| 919 |
+
@media (max-width: 900px) {
|
| 920 |
+
#models .cs-enter { grid-column: 1; justify-self: stretch; }
|
| 921 |
+
#models .mb-info { padding-top: 0; }
|
| 922 |
+
#models .mb-roster { margin-top: 0; }
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
/* ββ ROUND 15: THE PARTY READS AS CLASSES ββ */
|
| 926 |
+
#models .mb-dot { padding-right: 26px; }
|
| 927 |
+
.mb-dot-sigil {
|
| 928 |
+
position: absolute; right: 7px; bottom: 6px; width: 13px; height: 13px; opacity: 0.75;
|
| 929 |
+
}
|
| 930 |
+
.mb-dot-sigil svg { width: 100%; height: 100%; }
|
| 931 |
+
|
| 932 |
+
/* ββ ROUND 17: THE SPLASH YIELDS ββ */
|
| 933 |
+
.cs-splash { cursor: pointer; }
|
| 934 |
+
|
| 935 |
+
|
| 936 |
+
/* ββ ROUND 9: ATTENTION ββ */
|
| 937 |
+
#models .mb-art { transition: rotate 0.4s ease, transform 0.3s ease-out; perspective: 900px; }
|
| 938 |
+
|
| 939 |
+
/* ββ ROUND 11: THE SELECTION WIPE ββ A sheet of the class light crossing the
|
| 940 |
+
stage on every selection β the cut between characters reads as a cut. */
|
| 941 |
+
.cs-wipe {
|
| 942 |
+
position: absolute; inset: 0; z-index: 3; pointer-events: none; opacity: 0;
|
| 943 |
+
background: linear-gradient(100deg, transparent 30%,
|
| 944 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 26%, transparent) 50%,
|
| 945 |
+
transparent 70%);
|
| 946 |
+
transform: translateX(-100%);
|
| 947 |
+
}
|
| 948 |
+
.cs-wipe.cs-go { animation: csWipe 0.55s ease-out; }
|
| 949 |
+
@keyframes csWipe {
|
| 950 |
+
0% { opacity: 1; transform: translateX(-100%); }
|
| 951 |
+
100% { opacity: 1; transform: translateX(100%); }
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
/* ββ ROUND 16: THE ENGAGE ββ Clicking ENTER flashes the realm in the class
|
| 955 |
+
light and falls to black before the chat page takes over β an exit that
|
| 956 |
+
answers the entrance. Reduced motion (and modified clicks) skip it. */
|
| 957 |
+
.cs-engage {
|
| 958 |
+
position: absolute; inset: 0; z-index: 40; pointer-events: none; opacity: 0;
|
| 959 |
+
background:
|
| 960 |
+
radial-gradient(ellipse at 50% 60%,
|
| 961 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, transparent) 0%, transparent 60%),
|
| 962 |
+
#05070d;
|
| 963 |
+
}
|
| 964 |
+
.cs-engage.cs-go { animation: csEngage 0.34s ease-in forwards; }
|
| 965 |
+
@keyframes csEngage {
|
| 966 |
+
0% { opacity: 0; }
|
| 967 |
+
45% { opacity: 0.65; }
|
| 968 |
+
100% { opacity: 1; }
|
| 969 |
+
}
|
| 970 |
+
@media (prefers-reduced-motion: reduce) { .cs-wipe.cs-go, .cs-engage.cs-go { animation: none; } }
|
| 971 |
+
|
| 972 |
+
|
| 973 |
+
/* ββ ROUND 10: STAT ICONOGRAPHY + HONEST BARS ββ Icons name the stat; the
|
| 974 |
+
bars are within-character fractions only (context: shipped ceiling over
|
| 975 |
+
the checkpoint's own max; memory: the build's residency) β never a
|
| 976 |
+
cross-model ranking. */
|
| 977 |
+
.mb-stat-ico { display: inline-flex; width: 12px; height: 12px; margin-right: 7px;
|
| 978 |
+
vertical-align: -1px; color: color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, var(--dim)); }
|
| 979 |
+
.mb-stat-ico svg { width: 100%; height: 100%; }
|
| 980 |
+
.mb-bar { display: block; height: 3px; margin-top: 5px; border-radius: 2px;
|
| 981 |
+
background: #ffffff12; overflow: hidden; }
|
| 982 |
+
.mb-bar i { display: block; height: 100%; border-radius: 2px;
|
| 983 |
+
background: linear-gradient(90deg,
|
| 984 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, transparent),
|
| 985 |
+
var(--cs-accent, var(--accent)));
|
| 986 |
+
transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1); }
|
| 987 |
+
#models .mb-stats dd { min-width: 128px; }
|
| 988 |
+
|
| 989 |
+
/* ββ ROUND 13: THE LOADOUT GAUGE ββ Filled diamonds for what the build
|
| 990 |
+
keeps, hollow for what it streams β ordinal, not a measurement. */
|
| 991 |
+
.mb-gauge { font-size: 0.6rem; letter-spacing: 0.08em; margin-right: 7px;
|
| 992 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, var(--dim)); }
|
| 993 |
+
|
| 994 |
+
|
| 995 |
+
/* ββ ROUND 12: IDLE LIFE ββ Left alone, the realm breathes: the stage sways
|
| 996 |
+
a few pixels on a long period and the aura thickens slightly. Any input
|
| 997 |
+
snaps the camera back to the hand. */
|
| 998 |
+
#models .cs-root.cs-idle .mb-art { animation: idleSway 14s ease-in-out infinite; }
|
| 999 |
+
@keyframes idleSway {
|
| 1000 |
+
0%, 100% { transform: translate(0, 0); }
|
| 1001 |
+
25% { transform: translate(6px, -3px); }
|
| 1002 |
+
75% { transform: translate(-6px, 2px); }
|
| 1003 |
+
}
|
| 1004 |
+
#models .cs-root.cs-idle .cs-aura i { animation-duration: calc(3.8s / var(--cs-tempo, 1)); }
|
| 1005 |
+
|
| 1006 |
+
/* ββ ROUND 14: REALM WEATHER ββ A falling trace crosses the high sky every
|
| 1007 |
+
~16 s β thin, fast, gone. And the fog drinks a hint of the class light, so
|
| 1008 |
+
the weather itself changes when the character does. */
|
| 1009 |
+
.cs-comet {
|
| 1010 |
+
position: absolute; top: 12%; left: -8%; width: 130px; height: 2px;
|
| 1011 |
+
background: linear-gradient(90deg, transparent, #cfe0ffbb 60%, #fff);
|
| 1012 |
+
border-radius: 2px; opacity: 0; pointer-events: none; z-index: 0;
|
| 1013 |
+
transform: rotate(9deg);
|
| 1014 |
+
animation: comet 16s linear infinite;
|
| 1015 |
+
animation-delay: 5s;
|
| 1016 |
+
}
|
| 1017 |
+
@keyframes comet {
|
| 1018 |
+
0%, 93% { opacity: 0; left: -8%; top: 12%; }
|
| 1019 |
+
94% { opacity: 0.9; }
|
| 1020 |
+
100% { opacity: 0; left: 108%; top: 26%; }
|
| 1021 |
+
}
|
| 1022 |
+
.cs-fog i {
|
| 1023 |
+
background: radial-gradient(ellipse at center,
|
| 1024 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 12%, #18223a) 0%, transparent 65%);
|
| 1025 |
+
opacity: 0.36;
|
| 1026 |
+
}
|
| 1027 |
+
@media (prefers-reduced-motion: reduce) {
|
| 1028 |
+
.cs-comet, #models .cs-root.cs-idle .mb-art { animation: none; }
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
|
| 1032 |
+
/* ββ CHAT MODE ββ ENTER pressed: the roster and the sheet step aside, the
|
| 1033 |
+
conversation takes the right column, and the character stays on stage β
|
| 1034 |
+
summoning heartbeat while the weights land, faster heartbeat while it
|
| 1035 |
+
thinks, one bounce per real token while it speaks. Requires chat-ui.css
|
| 1036 |
+
(linked by index.html) for the message bubbles and the composer; below is
|
| 1037 |
+
only the panel chrome and the stagecraft. */
|
| 1038 |
+
|
| 1039 |
+
#models .cs-root.cs-chatting {
|
| 1040 |
+
/* The nav is sticky IN FLOW (60px + border), so a flat 100dvh hangs the
|
| 1041 |
+
composer 61px below the fold. In chat mode the composer is the primary
|
| 1042 |
+
control β the chamber must end above it. The tightened padding-top gives
|
| 1043 |
+
the reclaimed room to the conversation. */
|
| 1044 |
+
height: calc(100dvh - 61px);
|
| 1045 |
+
min-height: 0;
|
| 1046 |
+
padding-top: 22px;
|
| 1047 |
+
grid-template-columns: 1fr minmax(420px, 600px);
|
| 1048 |
+
grid-template-rows: auto 1fr auto;
|
| 1049 |
+
grid-template-areas:
|
| 1050 |
+
'plate chat'
|
| 1051 |
+
'stage chat'
|
| 1052 |
+
'note chat';
|
| 1053 |
+
}
|
| 1054 |
+
/* #models-prefixed: the base rules carry the #models id, and a bare class
|
| 1055 |
+
pair loses that specificity fight β the roster stayed visible and its
|
| 1056 |
+
orphaned grid-area spawned implicit tracks that crushed the real columns
|
| 1057 |
+
(caught by the first browser pass). */
|
| 1058 |
+
#models .cs-chatting .mb-info, #models .cs-chatting .mb-roster,
|
| 1059 |
+
#models .cs-chatting .cs-roster-head, #models .cs-chatting .cs-enter,
|
| 1060 |
+
#models .cs-chatting .mb-arrow, #models .cs-chatting .cs-corner-r { display: none; }
|
| 1061 |
+
|
| 1062 |
+
/* The engage flash no longer hands off to another page β it lifts to reveal
|
| 1063 |
+
the chamber. Starts opaque (the .cs-go fade ran under it), holds long
|
| 1064 |
+
enough to cover the swap, then clears. */
|
| 1065 |
+
#models .cs-root.cs-chatting .cs-engage.cs-go { animation: csEngageReveal 0.9s ease forwards; }
|
| 1066 |
+
@keyframes csEngageReveal {
|
| 1067 |
+
0%, 30% { opacity: 1; }
|
| 1068 |
+
100% { opacity: 0; visibility: hidden; }
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
/* The panel: the sheet's glass and rune corners, grown into an instrument. */
|
| 1072 |
+
#models .cs-chat {
|
| 1073 |
+
/* min-width 0: grid items default to min-content width, and the nowrap
|
| 1074 |
+
composer-hint line was wider than a phone β the panel overflowed the
|
| 1075 |
+
viewport instead of shrinking into its column. */
|
| 1076 |
+
grid-area: chat; z-index: 2; min-height: 0; min-width: 0; position: relative;
|
| 1077 |
+
/* It is a <section>, and the page's generic section rule pads 72px into
|
| 1078 |
+
the panel β zero it; the head/composer carry their own spacing. */
|
| 1079 |
+
padding: 0;
|
| 1080 |
+
display: flex; flex-direction: column;
|
| 1081 |
+
border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 22%, var(--border));
|
| 1082 |
+
border-radius: 4px;
|
| 1083 |
+
background: linear-gradient(180deg, #0c1220ee, #090e18e6);
|
| 1084 |
+
box-shadow: inset 0 1px 0 #ffffff0a, 0 18px 50px #0009;
|
| 1085 |
+
backdrop-filter: blur(4px);
|
| 1086 |
+
animation: csChatIn 0.5s cubic-bezier(0.2, 1, 0.3, 1) 0.1s both;
|
| 1087 |
+
}
|
| 1088 |
+
@keyframes csChatIn {
|
| 1089 |
+
from { opacity: 0; transform: translateX(26px); }
|
| 1090 |
+
to { opacity: 1; transform: translateX(0); }
|
| 1091 |
+
}
|
| 1092 |
+
#models .cs-chat::before, #models .cs-chat::after {
|
| 1093 |
+
content: ''; position: absolute; width: 40px; height: 40px; z-index: 3; pointer-events: none;
|
| 1094 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
|
| 1095 |
+
-webkit-mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
|
| 1096 |
+
mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
|
| 1097 |
+
}
|
| 1098 |
+
#models .cs-chat::before { top: -5px; left: -5px; }
|
| 1099 |
+
#models .cs-chat::after { bottom: -5px; right: -5px; transform: rotate(180deg); }
|
| 1100 |
+
|
| 1101 |
+
/* Panel header: sigil, name, build, live badge, tools. */
|
| 1102 |
+
.cs-chat-head {
|
| 1103 |
+
display: flex; align-items: center; gap: 10px;
|
| 1104 |
+
padding: 12px 14px; border-bottom: 1px solid #ffffff0f;
|
| 1105 |
+
}
|
| 1106 |
+
.cs-chat-sigil { flex: none; width: 22px; height: 22px;
|
| 1107 |
+
color: var(--cs-accent, var(--accent)); }
|
| 1108 |
+
.cs-chat-sigil svg { width: 100%; height: 100%; }
|
| 1109 |
+
.cs-chat-id { display: flex; flex-direction: column; gap: 1px; min-width: 0; margin-right: auto; }
|
| 1110 |
+
.cs-chat-id b { font-family: var(--display); font-weight: 600; font-size: 1.02rem;
|
| 1111 |
+
line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 1112 |
+
.cs-chat-id i { font-style: normal; font-family: var(--mono); font-size: 0.58rem;
|
| 1113 |
+
letter-spacing: 0.12em; text-transform: uppercase;
|
| 1114 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, var(--text));
|
| 1115 |
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 1116 |
+
.cs-chat-tool {
|
| 1117 |
+
flex: none; font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em;
|
| 1118 |
+
text-transform: uppercase; color: var(--dim); text-decoration: none;
|
| 1119 |
+
padding: 5px 9px; border: 1px solid #ffffff1c; border-radius: 3px; background: #ffffff07;
|
| 1120 |
+
cursor: pointer; transition: color 0.15s, border-color 0.15s;
|
| 1121 |
+
}
|
| 1122 |
+
.cs-chat-tool:hover { color: var(--text); border-color: var(--muted); }
|
| 1123 |
+
.cs-chat-tool:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 1124 |
+
|
| 1125 |
+
/* Live badge β written by loading-ui.ts (Summoning β % β Ready). */
|
| 1126 |
+
.cs-chat .badge {
|
| 1127 |
+
flex: none; display: inline-flex; align-items: center; gap: 6px;
|
| 1128 |
+
font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase;
|
| 1129 |
+
padding: 4px 9px; border: 1px solid #ffffff1c; border-radius: 999px; color: var(--dim);
|
| 1130 |
+
font-variant-numeric: tabular-nums; white-space: nowrap;
|
| 1131 |
+
}
|
| 1132 |
+
.cs-chat .badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); }
|
| 1133 |
+
.cs-chat .badge.loading { color: #fbbf24; border-color: #fbbf2440; }
|
| 1134 |
+
.cs-chat .badge.loading .dot { background: #fbbf24; animation: csBadgePulse 1.4s infinite; }
|
| 1135 |
+
.cs-chat .badge.ready { color: var(--cs-accent, var(--accent));
|
| 1136 |
+
border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, transparent); }
|
| 1137 |
+
.cs-chat .badge.ready .dot { background: var(--cs-accent, var(--accent)); }
|
| 1138 |
+
.cs-chat .badge.error { color: var(--err); border-color: color-mix(in srgb, var(--err) 45%, transparent); }
|
| 1139 |
+
.cs-chat .badge.error .dot { background: var(--err); }
|
| 1140 |
+
@keyframes csBadgePulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
| 1141 |
+
|
| 1142 |
+
/* The rite card: download + compile progress, then it folds away. */
|
| 1143 |
+
.cs-boot { padding: 16px 16px 14px; border-bottom: 1px solid #ffffff0f; }
|
| 1144 |
+
.cs-boot.cs-done { display: none; }
|
| 1145 |
+
.cs-boot-title { font-family: var(--display); font-size: 1.05rem; margin-bottom: 2px; }
|
| 1146 |
+
.cs-boot-status { font-size: 0.78rem; color: var(--text-dim, #a9b3c6); min-height: 1.2em; margin-bottom: 10px; }
|
| 1147 |
+
.cs-boot-track { height: 4px; border-radius: 2px; background: #ffffff12; overflow: hidden; }
|
| 1148 |
+
.cs-boot-track i {
|
| 1149 |
+
display: block; height: 100%; width: 0; border-radius: 2px;
|
| 1150 |
+
background: linear-gradient(90deg,
|
| 1151 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, transparent),
|
| 1152 |
+
var(--cs-accent, var(--accent)));
|
| 1153 |
+
transition: width 0.25s ease;
|
| 1154 |
+
}
|
| 1155 |
+
.cs-boot-detail { margin-top: 7px; font-family: var(--mono); font-size: 0.64rem;
|
| 1156 |
+
color: var(--dim); font-variant-numeric: tabular-nums; }
|
| 1157 |
+
.cs-boot-note { margin-top: 7px; font-size: 0.72rem; color: var(--warn, #d9a441); }
|
| 1158 |
+
.cs-boot-log { margin-top: 9px; }
|
| 1159 |
+
.cs-boot-log summary {
|
| 1160 |
+
font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.14em; text-transform: uppercase;
|
| 1161 |
+
color: var(--dim); cursor: pointer; user-select: none;
|
| 1162 |
+
}
|
| 1163 |
+
.cs-boot-log summary:hover { color: var(--text-dim, #a9b3c6); }
|
| 1164 |
+
.cs-boot-log pre {
|
| 1165 |
+
margin-top: 6px; max-height: 130px; overflow-y: auto;
|
| 1166 |
+
padding: 8px 10px; border: 1px solid #ffffff0f; border-radius: 3px; background: #070b13;
|
| 1167 |
+
font-family: var(--mono); font-size: 0.62rem; line-height: 1.5; color: var(--text-dim, #a9b3c6);
|
| 1168 |
+
white-space: pre-wrap;
|
| 1169 |
+
}
|
| 1170 |
+
.cs-boot-error { display: none; margin-top: 10px; padding: 9px 11px;
|
| 1171 |
+
border: 1px solid color-mix(in srgb, var(--err) 40%, transparent); border-radius: 3px;
|
| 1172 |
+
background: color-mix(in srgb, var(--err) 9%, transparent);
|
| 1173 |
+
font-size: 0.76rem; line-height: 1.5; color: color-mix(in srgb, var(--err) 60%, #fff); }
|
| 1174 |
+
.cs-boot-error.visible { display: block; }
|
| 1175 |
+
|
| 1176 |
+
/* Conversation column: bubbles ride the panel glass, not their own ground. */
|
| 1177 |
+
.cs-chat .chat-main { flex: 1; min-height: 0; }
|
| 1178 |
+
.cs-chat .welcome.cs-wait, .cs-chat .welcome.hidden { display: none; }
|
| 1179 |
+
.cs-chat .welcome { padding: 1.4rem 0.2rem 1rem; }
|
| 1180 |
+
.cs-welcome-title { font-family: var(--display); font-size: 1.3rem; margin-bottom: 6px; }
|
| 1181 |
+
.cs-welcome-lore { font-size: 0.8rem; font-style: italic; line-height: 1.55;
|
| 1182 |
+
color: color-mix(in srgb, var(--text) 62%, transparent); margin-bottom: 14px; }
|
| 1183 |
+
.cs-sug-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
| 1184 |
+
.cs-chat .suggest {
|
| 1185 |
+
text-align: left; font-size: 0.76rem; line-height: 1.4; color: var(--text-dim, #a9b3c6);
|
| 1186 |
+
padding: 10px 12px; border: 1px solid #ffffff14; border-radius: 3px; background: #ffffff06;
|
| 1187 |
+
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
|
| 1188 |
+
}
|
| 1189 |
+
.cs-chat .suggest:hover {
|
| 1190 |
+
color: var(--text); background: color-mix(in srgb, var(--cs-accent, var(--accent)) 8%, transparent);
|
| 1191 |
+
border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, transparent);
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
/* Composer, re-cut to the entrance's edge language (chat-ui.css carries the
|
| 1195 |
+
mechanics; only the chrome changes here). */
|
| 1196 |
+
.cs-chat .composer-wrap { background: transparent; border-top: 1px solid #ffffff0f; padding: 11px 13px 10px; }
|
| 1197 |
+
.cs-chat .composer { background: #0a101ccc; border-radius: 3px;
|
| 1198 |
+
border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 26%, var(--border)); }
|
| 1199 |
+
.cs-chat .composer:focus-within { background: #0c1322; border-color: var(--cs-accent, var(--accent)); }
|
| 1200 |
+
.cs-chat .composer-btn { border-radius: 3px; }
|
| 1201 |
+
.cs-chat .composer-hint {
|
| 1202 |
+
display: flex; justify-content: space-between; gap: 4px 10px; flex-wrap: wrap; text-align: left;
|
| 1203 |
+
font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.06em; color: var(--dim);
|
| 1204 |
+
max-width: none;
|
| 1205 |
+
}
|
| 1206 |
+
.cs-chat-stats { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
| 1207 |
+
|
| 1208 |
+
/* ββ Stagecraft: what the realm does while the model works ββββββββββββββββ */
|
| 1209 |
+
|
| 1210 |
+
/* Summoning: the circle beats slowly and the ring runs the armed sweep β the
|
| 1211 |
+
character is being drawn through. */
|
| 1212 |
+
#models .cs-summoning .mb-pedestal::after { animation: ringPulse 1.5s ease-in-out infinite; }
|
| 1213 |
+
#models .cs-summoning .mb-pedestal::before {
|
| 1214 |
+
content: ''; position: absolute; inset: 0;
|
| 1215 |
+
background: conic-gradient(from 0deg,
|
| 1216 |
+
transparent 0deg, transparent 290deg,
|
| 1217 |
+
color-mix(in srgb, var(--cs-accent, var(--accent)) 90%, #fff) 330deg,
|
| 1218 |
+
transparent 360deg);
|
| 1219 |
+
-webkit-mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
|
| 1220 |
+
mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
|
| 1221 |
+
animation: armedSpin 2.6s linear infinite;
|
| 1222 |
+
}
|
| 1223 |
+
/* Thinking: prefill β the ring tightens to a fast shallow breath. */
|
| 1224 |
+
#models .cs-thinking .mb-pedestal::after { animation: ringPulse 0.9s ease-in-out infinite; }
|
| 1225 |
+
#models .cs-thinking .mb-pedestal { filter: brightness(1.18); }
|
| 1226 |
+
/* Generating: the floor burns steady and bright; the bounce on stage is the
|
| 1227 |
+
token stream itself (mascot.pulse per emitted token). */
|
| 1228 |
+
#models .cs-generating .mb-pedestal { filter: brightness(1.3); }
|
| 1229 |
+
#models .cs-generating .mb-pedestal::after { animation: none; opacity: 1; }
|
| 1230 |
+
#models .mb-pedestal { transition: filter 0.4s ease; }
|
| 1231 |
+
|
| 1232 |
+
/* Phones: the character folds to a small familiar above the conversation. */
|
| 1233 |
+
@media (max-width: 900px) {
|
| 1234 |
+
#models .cs-root.cs-chatting {
|
| 1235 |
+
grid-template-columns: 1fr;
|
| 1236 |
+
grid-template-rows: auto auto 1fr;
|
| 1237 |
+
grid-template-areas: 'plate' 'stage' 'chat';
|
| 1238 |
+
}
|
| 1239 |
+
#models .cs-chatting .mb-mascot { height: 110px; -webkit-box-reflect: none; }
|
| 1240 |
+
#models .cs-chatting .mb-pedestal { height: 60px; }
|
| 1241 |
+
#models .cs-chatting .cs-lore { display: none; }
|
| 1242 |
+
#models .cs-chatting .mb-plate { padding-bottom: 4px; }
|
| 1243 |
+
#models .cs-chatting .mb-name { font-size: 1.3rem; }
|
| 1244 |
+
.cs-sug-grid { grid-template-columns: 1fr; }
|
| 1245 |
+
.cs-chat-tool#new-chat-btn { display: none; }
|
| 1246 |
+
}
|
| 1247 |
+
@media (prefers-reduced-motion: reduce) {
|
| 1248 |
+
#models .cs-chat, #models .cs-root.cs-chatting .cs-engage.cs-go { animation: none; }
|
| 1249 |
+
#models .cs-summoning .mb-pedestal::before { animation: none; }
|
| 1250 |
+
}
|
| 1251 |
+
|
| 1252 |
+
/* ββ DOWNLOADED = READY ββ The READY tag gains a fill so it reads at roster
|
| 1253 |
+
distance, and a variant chip that is already on this device carries a dot β
|
| 1254 |
+
both from the same OPFS probe as everything else. */
|
| 1255 |
+
#models .mb-dot[data-cached]::after {
|
| 1256 |
+
background: color-mix(in srgb, var(--ok) 16%, transparent);
|
| 1257 |
+
}
|
| 1258 |
+
#models .mb-variant[data-cached]::after {
|
| 1259 |
+
content: 'β'; margin-left: 7px; font-size: 0.5rem; vertical-align: 2px;
|
| 1260 |
+
color: var(--ok);
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
/* ββ GAME ROUND: abilities, saves, listening, the token stream βββββββββββββ */
|
| 1264 |
+
|
| 1265 |
+
/* Abilities: the spec's mechanics as passives β rune bullet, name, clause. */
|
| 1266 |
+
#models .mb-abilities { list-style: none; margin: 12px 0 0; padding: 0;
|
| 1267 |
+
display: flex; flex-direction: column; gap: 6px; }
|
| 1268 |
+
#models .mb-abilities li { display: flex; align-items: baseline; gap: 8px;
|
| 1269 |
+
font-size: 0.74rem; line-height: 1.4; }
|
| 1270 |
+
#models .mb-abilities li::before { content: 'β'; flex: none; font-size: 0.5rem;
|
| 1271 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff); }
|
| 1272 |
+
#models .mb-abilities b { flex: none; font-weight: 600; letter-spacing: 0.02em;
|
| 1273 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, var(--text)); }
|
| 1274 |
+
#models .mb-abilities span { color: var(--dim); }
|
| 1275 |
+
|
| 1276 |
+
/* Save slot: a stored conversation marks its character β roster glyph +
|
| 1277 |
+
sheet line. Presence only, never a count. */
|
| 1278 |
+
#models .mb-dot[data-save] .mb-dot-text b::after {
|
| 1279 |
+
content: ' β'; font-size: 0.7em; vertical-align: 1px;
|
| 1280 |
+
color: #cfe0ffaa;
|
| 1281 |
+
}
|
| 1282 |
+
#models .mb-save { font-size: var(--step--1); margin: 10px 0 0;
|
| 1283 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, var(--text)); }
|
| 1284 |
+
|
| 1285 |
+
/* LISTENING: keys are landing β the character turns toward the panel. The
|
| 1286 |
+
ears perk through the mascot's hover state; this is the body language. */
|
| 1287 |
+
#models .cs-chatting.cs-listening .mb-art { rotate: y 9deg; }
|
| 1288 |
+
#models .mb-art { transition: transform 0.3s ease-out, rotate 0.45s ease; }
|
| 1289 |
+
|
| 1290 |
+
/* THE TOKEN STREAM: while the model speaks, glyphs fly from the character's
|
| 1291 |
+
mouth into the conversation. Visible ONLY under cs-generating β the stream
|
| 1292 |
+
exists exactly when tokens do. */
|
| 1293 |
+
.cs-stream { display: none; }
|
| 1294 |
+
#models .cs-chatting.cs-generating .cs-stream { display: block;
|
| 1295 |
+
position: absolute; left: 26%; right: 0; top: 0; bottom: 0;
|
| 1296 |
+
pointer-events: none; z-index: 2; }
|
| 1297 |
+
.cs-stream i {
|
| 1298 |
+
position: absolute; left: 0; top: 44%;
|
| 1299 |
+
font-style: normal; font-size: 11px;
|
| 1300 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
|
| 1301 |
+
text-shadow: 0 0 6px color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, transparent);
|
| 1302 |
+
opacity: 0;
|
| 1303 |
+
animation: tokFly 1.15s linear infinite;
|
| 1304 |
+
}
|
| 1305 |
+
.cs-stream i:nth-child(2) { animation-delay: 0.19s; font-size: 8px; top: 45.5%; }
|
| 1306 |
+
.cs-stream i:nth-child(3) { animation-delay: 0.38s; top: 43%; }
|
| 1307 |
+
.cs-stream i:nth-child(4) { animation-delay: 0.57s; font-size: 8px; top: 46%; }
|
| 1308 |
+
.cs-stream i:nth-child(5) { animation-delay: 0.76s; top: 44.5%; }
|
| 1309 |
+
.cs-stream i:nth-child(6) { animation-delay: 0.95s; font-size: 9px; top: 43.5%; }
|
| 1310 |
+
@keyframes tokFly {
|
| 1311 |
+
0% { transform: translateX(0) translateY(0); opacity: 0; }
|
| 1312 |
+
12% { opacity: 0.9; }
|
| 1313 |
+
55% { transform: translateX(22vw) translateY(-1.2vh); opacity: 0.85; }
|
| 1314 |
+
92% { opacity: 0.5; }
|
| 1315 |
+
100% { transform: translateX(38vw) translateY(-0.6vh); opacity: 0; }
|
| 1316 |
+
}
|
| 1317 |
+
|
| 1318 |
+
/* SUMMON FAILURE: the circle goes cold. The rite card carries the flavour
|
| 1319 |
+
line; the diagnostic stays in the error panel. */
|
| 1320 |
+
#models .cs-boot-failed .mb-pedestal { filter: grayscale(0.7) brightness(0.55); }
|
| 1321 |
+
#models .cs-boot-failed .mb-pedestal::after { animation: none; opacity: 0.3; }
|
| 1322 |
+
#models .cs-boot-failed .mb-pedestal::before { display: none; }
|
| 1323 |
+
|
| 1324 |
+
@media (max-width: 900px) {
|
| 1325 |
+
/* The stream's geometry assumes stage-left/panel-right; stacked phones
|
| 1326 |
+
have neither, and the abilities pad the sheet. */
|
| 1327 |
+
.cs-stream { display: none !important; }
|
| 1328 |
+
}
|
| 1329 |
+
@media (prefers-reduced-motion: reduce) {
|
| 1330 |
+
.cs-stream { display: none !important; }
|
| 1331 |
+
#models .cs-chatting.cs-listening .mb-art { rotate: none; }
|
| 1332 |
+
}
|
| 1333 |
+
|
| 1334 |
+
/* ββ EXTENDED ROUND: sleep, the constellation, the deeds rail ββββββββββββββ */
|
| 1335 |
+
|
| 1336 |
+
/* ASLEEP (50 s idle): the ring cools and slows with the character β the
|
| 1337 |
+
sleepy face itself is the mascot's mood, this is the room's light. */
|
| 1338 |
+
#models .cs-asleep .mb-pedestal { filter: brightness(0.7) saturate(0.8); }
|
| 1339 |
+
#models .cs-asleep .mb-pedestal::after { animation-duration: 10s; }
|
| 1340 |
+
#models .cs-asleep .cs-fog i { opacity: 0.2; }
|
| 1341 |
+
|
| 1342 |
+
/* THE CONSTELLATION: the lane's sigil, huge and faint, behind the stage.
|
| 1343 |
+
Masked accent β it recolours with the character and drifts on parallax. */
|
| 1344 |
+
.cs-sigilbg {
|
| 1345 |
+
position: absolute; z-index: 0; pointer-events: none;
|
| 1346 |
+
left: 50%; top: 50%;
|
| 1347 |
+
width: min(74vh, 58vw); height: min(74vh, 58vw);
|
| 1348 |
+
margin: calc(min(74vh, 58vw) / -2) 0 0 calc(min(74vh, 58vw) / -2);
|
| 1349 |
+
background: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff);
|
| 1350 |
+
opacity: 0.05;
|
| 1351 |
+
-webkit-mask-size: contain; mask-size: contain;
|
| 1352 |
+
-webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
|
| 1353 |
+
-webkit-mask-position: center; mask-position: center;
|
| 1354 |
+
}
|
| 1355 |
+
#models .cs-chatting .cs-sigilbg { left: 27%; }
|
| 1356 |
+
|
| 1357 |
+
/* THE DEEDS RAIL: things that happened on this device. Unearned = a dim rune
|
| 1358 |
+
with the trigger in its tooltip (a quest list, not decoration); earned =
|
| 1359 |
+
lit, named. No counts anywhere. */
|
| 1360 |
+
#models .cs-deeds {
|
| 1361 |
+
grid-area: note; z-index: 1; justify-self: center;
|
| 1362 |
+
display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
|
| 1363 |
+
padding: 2px 0;
|
| 1364 |
+
}
|
| 1365 |
+
.cs-deed {
|
| 1366 |
+
font-family: var(--mono); font-size: 0.56rem; letter-spacing: 0.12em;
|
| 1367 |
+
text-transform: uppercase; color: color-mix(in srgb, var(--dim) 55%, transparent);
|
| 1368 |
+
cursor: default;
|
| 1369 |
+
}
|
| 1370 |
+
.cs-deed i { font-style: normal; display: none; margin-left: 5px; }
|
| 1371 |
+
.cs-deed[data-on] {
|
| 1372 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 75%, #fff);
|
| 1373 |
+
text-shadow: 0 0 8px color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, transparent);
|
| 1374 |
+
}
|
| 1375 |
+
.cs-deed[data-on] i { display: inline; }
|
| 1376 |
+
@media (max-width: 900px) { #models .cs-deeds { display: none; } }
|
| 1377 |
+
|
| 1378 |
+
/* ββ THE ROOM ββ Hosting from inside the game: the strip in the panel, and
|
| 1379 |
+
one small mascot per connected guest flanking the character. */
|
| 1380 |
+
.cs-room { padding: 12px 14px; border-bottom: 1px solid #ffffff0f; }
|
| 1381 |
+
.cs-room-consent p { font-size: 0.76rem; line-height: 1.55; color: var(--text-dim, #a9b3c6);
|
| 1382 |
+
margin: 0 0 10px; }
|
| 1383 |
+
.cs-room-linkrow { display: flex; gap: 6px; align-items: center; }
|
| 1384 |
+
.cs-room-linkrow input {
|
| 1385 |
+
flex: 1; min-width: 0; padding: 6px 9px; font-family: var(--mono); font-size: 0.66rem;
|
| 1386 |
+
color: var(--text); background: #0a101ccc; border-radius: 3px;
|
| 1387 |
+
border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 30%, var(--border));
|
| 1388 |
+
}
|
| 1389 |
+
.cs-room-members { margin-top: 8px; font-family: var(--mono); font-size: 0.62rem;
|
| 1390 |
+
letter-spacing: 0.08em; color: var(--dim); }
|
| 1391 |
+
.cs-room-log { list-style: none; margin: 8px 0 0; padding: 0; display: flex;
|
| 1392 |
+
flex-direction: column; gap: 4px; }
|
| 1393 |
+
.cs-room-log li { display: flex; gap: 8px; align-items: baseline; font-size: 0.7rem;
|
| 1394 |
+
color: var(--text-dim, #a9b3c6); overflow: hidden; white-space: nowrap; }
|
| 1395 |
+
.cs-room-log b { font-family: var(--mono); font-size: 0.6rem; font-weight: 400;
|
| 1396 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, var(--text)); flex: none; }
|
| 1397 |
+
.cs-room-log span { overflow: hidden; text-overflow: ellipsis; }
|
| 1398 |
+
.cs-room-log i { font-style: normal; font-family: var(--mono); font-size: 0.6rem;
|
| 1399 |
+
color: var(--dim); margin-left: auto; flex: none; }
|
| 1400 |
+
.cs-tool-live { border-color: var(--cs-accent, var(--accent)) !important;
|
| 1401 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff) !important; }
|
| 1402 |
+
|
| 1403 |
+
/* Guests on stage: small familiars on the ring's edge. */
|
| 1404 |
+
.cs-guest-mascot { position: absolute; width: 90px; height: 90px; z-index: 1;
|
| 1405 |
+
pointer-events: none; animation: guestIn 0.5s ease both; }
|
| 1406 |
+
.cs-guest-0 { left: 8%; bottom: 10%; }
|
| 1407 |
+
.cs-guest-1 { right: 8%; bottom: 10%; }
|
| 1408 |
+
.cs-guest-2 { left: 2%; bottom: 34%; }
|
| 1409 |
+
.cs-guest-3 { right: 2%; bottom: 34%; }
|
| 1410 |
+
.cs-guest-4 { left: 16%; bottom: 0; }
|
| 1411 |
+
.cs-guest-5 { right: 16%; bottom: 0; }
|
| 1412 |
+
@keyframes guestIn { from { opacity: 0; transform: translateY(10px) scale(0.8); }
|
| 1413 |
+
to { opacity: 1; transform: none; } }
|
| 1414 |
+
@media (max-width: 900px) { .cs-guest-mascot { display: none; } }
|
| 1415 |
+
|
| 1416 |
+
/* The room path under ENTER: quiet sibling, same class light. */
|
| 1417 |
+
#models .cs-enter { flex-direction: column; align-items: center; gap: 8px; }
|
| 1418 |
+
#models .mb-cta-room {
|
| 1419 |
+
font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.1em;
|
| 1420 |
+
text-transform: uppercase; text-decoration: none; cursor: pointer;
|
| 1421 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 65%, var(--dim));
|
| 1422 |
+
padding: 4px 10px; border: 1px solid transparent; border-radius: 3px;
|
| 1423 |
+
transition: color 0.15s, border-color 0.15s;
|
| 1424 |
+
}
|
| 1425 |
+
#models .mb-cta-room:hover {
|
| 1426 |
+
color: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
|
| 1427 |
+
border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, transparent);
|
| 1428 |
+
}
|
| 1429 |
+
|
| 1430 |
+
/* ββ LENS ROUND 2026-08-17 FIXES βββββββββββββββββββββββββββββββββββββββββββ */
|
| 1431 |
+
|
| 1432 |
+
/* The cinematic bottom fade must not wash the chat panel's composer β the
|
| 1433 |
+
panel IS the bottom of the screen in chat mode. */
|
| 1434 |
+
#models .cs-root.cs-chatting::after { display: none; }
|
| 1435 |
+
|
| 1436 |
+
@media (prefers-reduced-motion: reduce) {
|
| 1437 |
+
/* The mood states re-armed the ring pulse that the base reduce block had
|
| 1438 |
+
disabled; and guests should simply appear. */
|
| 1439 |
+
#models .cs-summoning .mb-pedestal::after,
|
| 1440 |
+
#models .cs-thinking .mb-pedestal::after { animation: none; }
|
| 1441 |
+
.cs-guest-mascot { animation: none; }
|
| 1442 |
+
}
|
og.png
ADDED
|
Git LFS Details
|