golem

module
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT

README ยถ

golem

Gemma, Qwen and Pocket TTS in a single static Go binary.
No Python. No cgo. No GPU required โ€” but with -vulkan it matches or exceeds llama.cpp's Vulkan performance on AMD hardware.

test Go Reference MIT


A golem is inert matter given a voice. That is what these engines do to a file of weights.

Golem is a set of inference engines written in pure Go. Run Gemma 4, Qwen3, Qwen3.8 and Kyutai Pocket TTS locally with go build, a GGUF file, and your CPU โ€” or your Vulkan GPU.

โœจ Features

  • Zero Friction: Compiles to a single static binary. No Python environment, no cgo, no runtime to install.
  • Pure Go, four dependencies: purego for the Vulkan loader, and three file formats the standard library does not read โ€” WebP, MP3, FLAC. CGO_ENABLED=0 go build ./... passes.
  • OpenAI Compatible: Drop-in replacement for OpenAI API clients, tool calls included.
  • Multimodal: Text, Vision (images) and Audio (WAV/MP3/FLAC) via Gemma 4.
  • Verified, not asserted: no layer is deemed correct until its intermediate activations match llama.cpp or PyTorch, waypoint by waypoint.
  • Fast on CPU: keeps pace with llama.cpp on tuned AVX2 kernels โ€” ahead reading prompts, level generating, except on the smallest model, where the weights stop being the cost and it says so.
  • Vulkan GPU: bound through purego rather than cgo. Measured on AMD against llama.cpp's own Vulkan build: ahead of it reading prompts on all five models, and generating on both Gemma ones. The table below says where it is behind, and by how much.
  • Serves several clients at once: -parallel N holds N conversations and carries a token for each of them through one read of the weights, on the card as well as on the processor โ€” Qwen3.8 on the card excepted, for a reason written below.

๐Ÿš€ Quickstart

Download a GGUF and run. No configuration file โ€” the engine reads general.architecture out of the file and opens whichever engine implements it, so no command here names a model family.

CLI (chat in the terminal)

go build ./cmd/golem-cli

./golem-cli -model gemma-4-E2B-it-QAT-Q4_0.gguf -p "Explain a mutex in one sentence." -stats

Server (OpenAI-compatible API)

go build ./cmd/golem-server

./golem-server -model Qwen3-4B-Q4_0.gguf -addr 127.0.0.1:8080

-parallel N cuts the context into N slots, each holding its own conversation.

๐Ÿ‘ฅ Several conversations, one pass

Whatever is waiting at the moment a pass is built goes into that pass. Four clients each wanting a token are four tokens in one read of a gigabyte of weights, rather than four reads โ€” llama.cpp's continuous batching, and it now works on the card as well as on the processor. The context is cut between the slots rather than multiplied, so -context 4096 -parallel 4 is four conversations of 1024 and the memory is what it was.

Gemma 4 26B A4B on an RX 9070 XT, drawing a token for each conversation from 448 positions of context:

conversations 1 2 4 8
tokens a second 82.1 125.9 211.4 255.0

Through the API, with prompts of about five hundred and forty tokens: one client reads 4055 positions a second and draws 75.3; two clients read 4260 between them and draw 115.6.

What made it possible is that a column now says which conversation it belongs to as well as where it sits, so a block's cache on the card is one ring per conversation rather than one ring. cmd/golem-server/README.md has the rest.

Qwen3.8 on the card is the exception, and -parallel above 1 is refused there rather than fallen back from. Forty-eight of its sixty-four blocks are delta nets, and a delta net keeps a state matrix a head that every token rewrites rather than a ring indexed by position: there is no ring to cut into slots, and answering two conversations off one state is a wrong answer and not a slow one. On the processor it holds slots like the rest.

โšก Vulkan GPU

-vulkan puts the whole model on the card. Measured on an RX 9070 XT, the same evening, against llama.cpp's own Vulkan build on the same files:

tokens a second golem gen llama gen golem pp64 llama pp64 golem pp256 llama pp256 golem pp512 llama pp512
Gemma 4 26B A4B 133.5 124.8 2061 833 3951 2918 4541 4038
Gemma 4 12B 65.4 64.6 1499 983 2611 2471 2858 2976
Qwen3 4B 167.0 169.7 3950 2952 5854 4545 5895 6125
Qwen3 0.6B 359.4 365.4 13915 9966 23787 19159 23995 22306
Qwen3.8 27B 30.1 32.8 799 628.7 1134 1127.8 1236 1234.7

Reading a prompt, golem is ahead on all five models at 64 and 256 positions, and on the 26B A4B, the 0.6B and Qwen3.8 at 512 as well. On the 26B A4B that is a factor of two and a half at sixty-four positions; on Qwen3.8 the two engines land within a tenth of a per cent of each other at 512, which the section below takes apart op by op.

Generating, golem is ahead on both Gemma models and within two per cent on both Qwen3 ones. Qwen3.8 generates slightly behind due to the heavy per-token state updates of its gated delta net, which cannot be amortised across a batch.

The card holds the whole model: 12.8 GiB for the 26B A4B, which is why sixteen is the smallest card that can run it, and about nine seconds of upload. -vulkan is all or nothing and says so: a card without VK_KHR_shader_integer_dot_product, a machine with no Vulkan loader, a model too large for the card โ€” each is an error at startup rather than a silent half-move. Without the flag, everything runs on the CPU as before.

(See ARCHITECTURE.md for the kernel work behind these numbers.)

๐Ÿ”ฎ Qwen3.8 drafts its own next token

The Qwen3.8 checkpoint ships a sixty-fifth block: a multi-token-prediction head that guesses the token after the one just decided, from the state the trunk has already computed. Guess right and the next pass verifies two tokens for the price of one. Guess wrong and it costs the pass it rode on and nothing else โ€” every token returned is drawn from the model's own distribution, so this needs none of the accept-reject correction that drafting with a separate model does. The answer is the same with drafting on and off, and a test asserts it.

Qwen3.8 27B, a seventeen-token prompt, greedy:

prompt a token at a time drafting drafts accepted
RX 9070 XT 119.6 /s 31.1 t/s 46.0 t/s 70%
i7-9700K, 8 threads 0.7 /s 0.70 t/s refused โ€”

Seventeen positions is a narrow pass, which is why the card reads this prompt at a hundred and twenty a second and a five-hundred-and-twelve-position one at 1236 โ€” the section below is that curve.

Drafting is refused on the processor, and the reason is one measurement. A speculative step costs a draft plus a pass of two columns, against the one-column pass it hopes to replace:

cost, as a fraction of one token on the card on the processor
the draft 0.08 0.03
the pass of two columns 0.94 1.97
drafts that must be accepted to break even 2% 99%

The card reads a block's weights once whether the pass carries one column or two, so verifying two tokens costs what drawing one did. The processor at this size is bound by arithmetic rather than by reading the weights, so two columns cost two columns โ€” and no acceptance rate can pay for that. qwen35/cost_test.go is where both tables come from.

๐Ÿ“ˆ Reading a prompt on the card

The same bargain reads a prompt. A pass carries up to five hundred and twelve positions, and what one costs says how far that goes:

positions in the pass 1 16 32 64 128 256 512
the pass 31.2ms 70.0ms 63.2ms 80.1ms 140.2ms 225.8ms 417.8ms
positions a second 32 229 506 799 913 1134 1236

Thirty-two positions cost less than sixteen, and that is not a misprint: sixteen is the widest the mat-vec builds, and thirty-two is where the tiled products take over.

A prompt reads at 1236 positions a second, against 62 when a pass carried two. llama.cpp's Vulkan build reads the same file on the same card at 1234.67 ยฑ 2.00 (llama-bench -p 512 -r 3). Both benchmarks do the same work โ€” 512 tokens from an empty cache, both warm, both synchronised, load time excluded โ€” with one difference, and it is ours: golem reads every position's hidden state back over the bus where llama.cpp keeps only the last.

Where a pass goes

Both engines report their own GPU timers op by op โ€” vk.Timeline here, GGML_VK_PERF_LOGGER=1 there. One pass of 512 positions of the 27B, in milliseconds:

golem llama.cpp
feed forward โ€” gate and up 132.6 149.0
feed forward โ€” down 97.9 95.6
delta net โ€” q, k, v, gate, ฮฑ, ฮฒ 55.1 60.9
attention 32.4 2.1
delta net โ€” output projection (Q5_K) 24.1 28.0
SwiGLU 17.1 11.2
recurrence, with its normalisations 15.9 23.2
attention โ€” q, k, v 14.4 15.6
everything else 26.4 38.8
the pass 416.0 424.4

The card is busy 97.6% of that wall clock โ€” the timer's ticks account for all but 2.4% of it โ€” so nothing measurable is lost between dispatches, and every difference above is a kernel.

Attention is the one line where the gap is a shape rather than a margin: golem dispatches the scores, the softmax and the mix as three passes over memory, where llama.cpp fuses them and keeps the block of scores where it computed it. Sixteen of the sixty-four blocks attend at all, which is why thirty milliseconds buys the other engine so much here and why this is the next line to take.

๐Ÿง  Supported Models

Family What it runs On CPU, vs its reference
Gemma 4 E2B, 12B, 26B A4B (mixture of 128 experts). Text, Vision, Audio. Reading a prompt ร—1.24 (E2B), ร—1.33 (12B), ร—1.06 (26B A4B). Generating, a tie: ร—1.01, ร—1.04, ร—1.06 โ€” vs llama.cpp
Qwen3 Dense models, from a GGUF. 4B: ร—1.13 reading, ร—1.00 generating. 0.6B: ร—0.99 reading, ร—0.85 generating โ€” vs llama.cpp
Qwen3.8 27B: Dense model featuring forty-eight gated delta nets and sixteen attentions (three to one ratio) โ€” plus the checkpoint's own multi-token-prediction head, which drafts the second token of every pass. 0.72 t/s on an i7-9700K: a delta net rewrites a 128ร—128 state a head every token, and that is arithmetic no kernel makes cheaper. On a card, 30.1 a token at a time, 46.0 drafting and 1236 reading a prompt, against llama.cpp's Vulkan build at 32.8 and 1234.7
Pocket TTS 13 shipped models across 6 languages, voice cloning included. ร—2.31 and ร—1.69 the speed of the PyTorch reference, on the 24- and 6-layer models

In absolute terms, on an i7-9700K with eight threads and Q4_0 weights: Gemma E2B draws 22.6 tokens a second and reads 204; the 12B, 5.0 and 42; the 26B A4B, 13.1 and 51; Qwen3 4B, 14.6 and 110. Pocket TTS speaks at ร—2.94 real time in French, ร—6.81 in English.

The 0.6B is the one this engine loses, and qwen/README.md says why: at 320 MB the weights fit close enough that the memory bus stops being the limit, and what is left is arithmetic, where llama.cpp's kernels win. This engine is built for the regime where reading the weights is the cost, and it says so where it is not.

๐Ÿ‘๏ธ Multimodal (Vision & Audio)

Provide the projector weights, and Gemma can see and hear:

Analyze images:

./golem-cli -model gemma-4-E2B-it-QAT-Q4_0.gguf \
    -mmproj mmproj-gemma-4-E2B-it-QAT-BF16.gguf \
    -image photo.png -p "What is in this picture?"

Transcribe and answer from audio:

./golem-cli -model gemma-4-E2B-it-QAT-Q4_0.gguf \
    -mmproj mmproj-gemma-4-E2B-it-QAT-BF16.gguf \
    -audio question.wav -p "Answer what is asked."

WAV, MP3 and FLAC, at any rate and any number of channels; the front end downmixes and resamples before the encoder sees anything. The 26B's projector carries no audio weights, so that checkpoint sees and does not hear.

The server reads the OpenAI content parts a client sends โ€” image_url for a picture, input_audio for a recording โ€” as a data: URI, as base64, or as a path on the machine it runs on. It does not fetch either over the network: a server that fetches what a prompt names is a server that can be aimed.

๐Ÿ—ฃ๏ธ Speech (Pocket TTS)

go build ./cmd/pocket-tts

./pocket-tts -voice voice.safetensors -o hello.wav "Bonjour le monde."

Clone a voice from a recording, without training and without leaving Go โ€” twenty to thirty seconds of mono 24 kHz is enough:

./pocket-tts -clone someone.wav -save-voice someone.safetensors
./pocket-tts -voice someone.safetensors -o answer.wav "And now I speak in that voice."

๐Ÿ”ฌ The Method

No layer is deemed correct until its intermediate activations match the reference implementation.

Scripts load the real weights, inject a deterministic input, and write every intermediate quantity into testdata/. The Go tests read those files back, so they need neither Python nor llama.cpp at test time. For gemma/ the reference is llama.cpp itself, instrumented, because a bf16 reference would bury a mistake under its own quantization error; for pockettts/ it is PyTorch, layer by layer, to a few parts in a million end to end.

Every number in this README is a benchmark in this repository, run on the machine named beside it. Nothing is estimated.

๐Ÿ› ๏ธ Project Structure

  • cmd/golem-cli, cmd/golem-server, cmd/pocket-tts โ€” the three commands.
  • engine/ โ€” reads the architecture out of a GGUF and opens the engine that implements it.
  • gemma/, qwen/, qwen35/, pockettts/ โ€” standalone engine implementations; they do not import one another. qwen35/ is Qwen3.8: a package is named for the architecture the GGUF declares, and this checkpoint declares general.architecture = qwen35, as llama.cpp's own models/qwen35.cpp does.
  • nn/ & vk/ โ€” the shared kernels: quantized AVX2 and NEON, and Vulkan compute.
  • tensors/, token/, chat/, sample/, audio/, imageio/ โ€” the rest of the shared layer.
  • ref/ โ€” what recorded each test fixture, and how to record it again.

๐Ÿค Contributing

We want to make Golem the best pure-Go inference engine available. We especially need:

  1. ARM benchmarks: the arm64 kernels are correct and tuned by nobody โ€” written and verified under emulation, never once timed on real hardware. Run ./benchmark-arm.sh on Apple Silicon or Graviton and share the results.
  2. Bug reports: if a test comparing against PyTorch or llama.cpp fails on your setup, please open an issue.
  3. Kernel optimization: help tune the NEON kernels for ARM64.
go build ./...
go test ./...

Weights are not in this repository, and every test that needs one skips cleanly when it cannot find it.

๐Ÿ“œ License & Credits

Golem is MIT Licensed.

Standing on the shoulders of giants: llama.cpp & ggml, Kyutai Pocket TTS, Google Gemma.

Directories ยถ

Path Synopsis
audio
decode
Package decode turns an encoded sound file into samples.
Package decode turns an encoded sound file into samples.
mel
Package mel turns a waveform into the log-mel spectrogram a speech model expects.
Package mel turns a waveform into the log-mel spectrogram a speech model expects.
resample
Package resample makes a signal mono and puts it at the rate a model wants.
Package resample makes a signal mono and puts it at the rate a model wants.
wav
Package chat holds the shape of a conversation, and the interface an engine implements to write one out.
Package chat holds the shape of a conversation, and the interface an engine implements to write one out.
cmd
golem-cli command
Command golem-cli holds a conversation with a GGUF model, on the CPU.
Command golem-cli holds a conversation with a GGUF model, on the CPU.
golem-server command
Command golem-server answers an OpenAI-compatible API over a GGUF, on the CPU.
Command golem-server answers an OpenAI-compatible API over a GGUF, on the CPU.
pocket-tts command
Command pocket-tts synthesizes text into a WAV file.
Command pocket-tts synthesizes text into a WAV file.
Package engine picks which engine reads a file, so that a command does not have to.
Package engine picks which engine reads a file, so that a command does not have to.
Package gemma runs Google's Gemma 4 language models from a GGUF file, in Go, with no cgo and nothing outside the standard library.
Package gemma runs Google's Gemma 4 language models from a GGUF file, in Go, with no cgo and nothing outside the standard library.
Package imageio decodes images and puts them in the shape a vision encoder reads: planar RGB, one float per channel per pixel.
Package imageio decodes images and puts them in the shape a vision encoder reads: planar RGB, one float per channel per pixel.
Package pockettts synthesizes speech from text with the Kyutai Pocket TTS models, in pure Go.
Package pockettts synthesizes speech from text with the Kyutai Pocket TTS models, in pure Go.
Package qwen runs Qwen3 dense language models from a GGUF file, in Go, with no cgo and nothing outside the standard library.
Package qwen runs Qwen3 dense language models from a GGUF file, in Go, with no cgo and nothing outside the standard library.
Package sample turns a row of logits into a token.
Package sample turns a row of logits into a token.
token
bpe
Package bpe reads Gemma 4's tokenizer out of a GGUF and applies it.
Package bpe reads Gemma 4's tokenizer out of a GGUF and applies it.
bytebpe
Package bytebpe reads a byte-level BPE tokenizer out of a GGUF and applies it โ€” the kind GPT-2 introduced and Qwen uses.
Package bytebpe reads a byte-level BPE tokenizer out of a GGUF and applies it โ€” the kind GPT-2 introduced and Qwen uses.
merge
Package merge applies a ranked byte-pair merge table to a list of symbols.
Package merge applies a ranked byte-pair merge table to a list of symbols.
special
Package special cuts a vocabulary's special tokens out of text before the rest of the tokenizer sees it.
Package special cuts a vocabulary's special tokens out of text before the rest of the tokenizer sees it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL