Skip to content
hybridgroupPublic

About

Go make a decision! Run System One models, 100% local in your browser using WebAssembly written in Go.

Resources

Stars

12 stars

Watchers

0 watching

Forks

Latest commit

 

History

16 Commits

Folders and files

Repository files navigation

Jeyzma

Go make a decision! Run System One models, 100% local in your browser using WebAssembly written in Go.

Jeyzma in a browser

https://jeyzma.com

Jeyzma asks a model typed questions about a state and shows a probability for each option. The answer comes from one forward pass, with no generated text. There is no server, no API key, and no data leaves your machine. It uses the GPU through WebGPU when available, otherwise the CPU. Written in Go using yzma on TinyGo.

How it works

yzma logo

The code is written in Go and compiled with TinyGo. It uses the yzma exp/decide package to run llama.cpp, compiled to a WebAssembly module. Everything runs in a Web Worker so the page stays responsive.

   index.html
       |  postMessage
   worker.js
     |            \
   yzma.wasm       yzma_wasm*.js
   (Go, TinyGo) -> (llama.cpp, Emscripten)

exp/decide renders the state and each question in the format the model was trained on, then reads the probabilities of the option tokens. DecideMany decodes the state once and shares it across all the questions.

Build and run

You need TinyGo 0.41 or later, Go 1.26, and node for the test.

make build
make serve

Open http://localhost:8080, click Load, and wait for the model to download. The browser caches it. Then click Decide.

The list has six models.

Model Size Readout Config
Julia-1 approximately 170 MB gguf none
Laya approximately 450 MB gguf none
d1 3B approximately 1.7 GB gguf none
Jev-Style 0.8B Decision v3 approximately 530 MB jev readout_config.json
JevK5 2B v0.2 approximately 2.0 GB jevk5 jevk5_config.json
decider 0.8B approximately 530 MB decider decider_config.json

A gguf model holds its readout, prompt template, and temperatures in the GGUF file, as converted for the llama-server /v1/systemone API. It loads with decide.Open and needs no config. The others need their config file too. Lev, OpenJev, Kev, Clef, and pplx-decider also load as gguf, but their files are larger than 2 GB, the most a browser can hold.

Choose Another model to enter your own model URL, config URL, and readout. Any URL works if the host sends CORS headers. Hugging Face does.

make build downloads about 13 MB of llama.cpp into build/, compiles the Go program, and copies the page. The only binary files in the repository are the social card and the two screenshots.

The download comes from llama-cpp-builder. It uses v0.7.0, the release that yzma v1.30.0 installs. It has shim ABI 11. DecideMany needs ABI 10 or later to share one state across questions, and d1 needs v0.7.0. To use another build, pass its tag, or latest for the newest nightly build.

make build LLAMA_VERSION=latest

Questions

The questions box holds a JSON list. Each question has a question and optional type and options.

Type Options Answer
choice a list of names, or an object of name to description one of the names
noul none, or {"false": "...", "true": "..."} true or false
score a list of levels in order the index of a level

Without a type, a question with options is a choice and one without is noul.

[
  {"question": "Which team handles this?", "options": {"billing": "payments, invoices", "technical": "bugs"}},
  {"question": "The customer is angry."},
  {"type": "score", "question": "How urgent is this?", "options": ["low", "medium", "high"]}
]

The state is any text. A JSON object or list keeps its key order.

Exact mode gives each question the same result it gets alone. Batched mode is faster but can differ slightly, and on a near tie it can change a JevK5 answer. A change of mode loads the model again.

Both orders reads each choice question a second time with the options in reverse order, and averages the two. This cancels a preference for the first options. It also loads the model again.

Each answer shows its confidence. A score question also shows the expected level, which is the mean of the levels weighted by their probabilities.

Play

/plays/pacman is Pac-Man with a model playing along. You play, and at each junction the model gets the same board as a choice question and picks a direction. The page shows its probabilities next to your move and counts how often you agree. Wait for the model stops the game at each junction until the model answers, so every turn is compared.

Pac-Man with a model playing along

The game is HTML5 Pac-Man by Dale Harvey, under the WTFPL. The copy in web/plays/pacman adds hooks so the page can read the board and hold the game at a junction. The audio and the font are left out.

The /v1/systemone request

Set Input to the request to send the same JSON that llama-server takes at /v1/systemone. The page answers it with decide.ParseRequest and Decider.Answer, and shows the response JSON under the bars.

{
  "state": {"ticket": "I was charged twice for my subscription and want a refund."},
  "questions": {
    "team": {"type": "choice", "instructions": "Which team handles this?", "criteria": {"billing": "payments, invoices", "technical": "bugs"}},
    "angry": {"type": "noul", "instructions": "The customer is angry."},
    "urgency": {"type": "score", "instructions": "How urgent is this?", "criteria": ["low", "medium", "high"]}
  }
}

Page calls

The Go program sets these functions for worker.js.

Function What it does
yzmaDecideLoad(modelURL, configURL, readout, manyMode, orders) Downloads a model, and its config unless readout is gguf. readout is jev, jevk5, decider, or gguf. manyMode is exact or batched. orders is both or empty.
yzmaDecideOpen(path, configJSON, readout, manyMode, orders) Loads a model that is already in the module's filesystem.
yzmaDecide(state, questionJSON, category) Scores one question.
yzmaDecideMany(state, questionsJSON, category) Scores a JSON list of questions about one state.
yzmaAnswer(requestJSON) Answers a /v1/systemone request.

The test

The test asks three questions in Node, without a browser. It fails when DecideMany and Decide differ in exact mode. --request also sends the questions as a /v1/systemone request and fails when the answers differ from DecideMany. --both turns on both orders.

make test READOUT=gguf MODEL=~/models/Julia-1-Q8_0.gguf TEST_FLAGS="--request"
make test READOUT=gguf MODEL=~/models/Laya-Q8_0.gguf
make test READOUT=gguf MODEL=~/models/d1-3B-Q4_K_M.gguf
make test MODEL=~/models/Jev-Style-0.8B-Decision-v3-Q4_K_M.gguf \
  CONFIG=~/models/readout_config.json TEST_FLAGS="--expect billing,true"
make test READOUT=jevk5 MODEL=~/models/jevk5-2b-v0.2-Q8_0.gguf \
  CONFIG=~/models/jevk5_config.json
make test READOUT=decider MODEL=~/models/decider-0.8b.Q4_K_M.gguf \
  CONFIG=~/models/decider_config.json

WebGPU, threads, and the service worker

llama.cpp has three WebAssembly builds. yzma-loader.js picks the best one the browser can run.

Build What it needs
yzma_wasm_webgpu WebGPU with f16 shaders, and JSPI. Chrome and Edge 137 or later.
yzma_wasm_mt SharedArrayBuffer, so a page with the COOP and COEP headers.
yzma_wasm Nothing. It runs everywhere.

The loader tests the GPU against the CPU before it uses it, and falls back to the CPU if the GPU is missing or computes wrong values. WebGPU in Firefox is much slower than the CPU, so the loader picks the CPU there.

GitHub Pages cannot send the COOP and COEP headers, so the page loads coi-serviceworker.js first. It adds the two headers and reloads the page once, so the loader can pick the multithreaded build. make serve sets the headers too.

The top right of the page shows the selected build. Add ?mode=cpu or ?mode=webgpu to the URL to force a build, and ?gpu=high-performance or ?gpu=low-power to pick the GPU on a machine with two. ?embed hides the paragraph at the top of the page.

The yzma WebAssembly guide has more on the backends, the browsers, and how fast each one is.

Notes

  • The model must be smaller than 2 GB, the limit of one JavaScript ArrayBuffer.
  • The CPU build is slow for the 2B model. Three questions take about 17 seconds on 16 threads in Node.
  • Discrete NVIDIA cards do not expose f16 shaders in Chrome. Start Chrome with --enable-dawn-features=vulkan_enable_f16_on_nvidia to use one.

Deploying

.github/workflows/pages.yml builds and deploys on each push to main. Set Settings → Pages → Source to GitHub Actions once.

The page is served at jeyzma.com. Enter jeyzma.com under Settings → Pages → Custom domain, and point the DNS at GitHub Pages with A records for 185.199.108.153, 185.199.109.153, 185.199.110.153, and 185.199.111.153, and a CNAME record for www to hybridgroup.github.io. A deploy from Actions does not read a CNAME file, so the setting is the only place the domain lives.

Social card

web/social-card.png is the preview image that social sites show for a link to jeyzma.com. Its source is images/social-card.svg. Render it again after a change.

inkscape images/social-card.svg --export-type=png --export-filename=web/social-card.png -w 1200 -h 630

License

Apache 2.0, the same as yzma. web/coi-serviceworker.js is MIT and keeps its own notice.

About

Go make a decision! Run System One models, 100% local in your browser using WebAssembly written in Go.

Resources

Stars

12 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages