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:
- 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.
- Bug reports: if a test comparing against PyTorch or llama.cpp fails on your setup, please open an issue.
- 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.