GPU kernels that say what they can do.
Write a tile kernel once. The compiler checks it, refuses what is wrong and names the rule, and writes CUDA C and Metal. The same kernels run Qwen3.8‑27B on a Mac's GPU.
Transcript
# cancho-gpu: typed tile kernels, one source for CUDA C and Metal
$ cat swiglu.chx
// a = silu(g) * u over a row of n: the gated activation of a SwiGLU MLP.
algo swiglu(n)
in g: f32[1, n]
in u: f32[1, n]
out y: f32[1, n]
{
let gt = load g
let ut = load u
let sg = sigmoid >
let a = gt * sg
store a * ut -> y
}
schedule swiglu for apple-m-series { threads 256 }
schedule swiglu for nvidia-ada { threads 256 }
$ cancho-gpu emit swiglu.chx out n=17408
swiglu_17408.cu: 256 threads, grid 1x1, 0 B of tiles
swiglu_17408.metal: 256 threads, grid 1x1, 0 B of tiles
# what it writes for Apple GPUs (the CUDA C is written next to it)
$ sed -n '/kernel void/,/^}/p' out/swiglu_17408.metal | head -22
kernel void swiglu_17408(
device const float* p0_g [[buffer(0)]],
device const float* p1_u [[buffer(1)]],
device float* p2_y [[buffer(2)]],
uint tid [[thread_index_in_threadgroup]],
uint3 tgpos [[threadgroup_position_in_grid]])
{
const uint gid = tgpos.x;
float v2[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v2[k] = float(1.0f / (1.0f + precise::exp(-float(float(p0_g[(uint)(0) * 17408u + (uint)(0) * 1u + ((e))])))));
}
}
float v3[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v3[k] = float(float(float(p0_g[(uint)(0) * 17408u + (uint)(0) * 1u + ((e))])) * float(v2[k]));
}
}
# a wrong program is refused, with the rule it broke
$ cat leak.chx
algo k(n)
in x: f32[1, n]
in w: f32[1, n]
out y: f32[1, n]
{
let a = load x
let b = load w
store sigmoid a -> y
}
schedule k for nvidia-ada { threads 32 }
$ cancho-gpu check leak.chx n=8
error[leak]: `w.1` is never consumed; drop it, yield it or move it into an op
# and the kernels run a 27B model on this Mac's GPU
$ cancho-gpu-run35 ~/.ollama/models kernels "Q: Why is the sky blue?\nA:" 64 --stream --timing
The sky appears blue due to a phenomenon called **Rayleigh scattering**.
When sunlight enters the Earth's atmosphere, it collides with gas molecules (primarily nitrogen and oxygen). Sunlight is composed of all colors of the visible spectrum, which have different wavelengths. Shorter wavelengths (like blue and violet) scatter
timing: load_ms=4580 prefill_tokens=11 prefill_ms=311 decode_tokens=63 decode_ms=2808Refuses wrong programs
Every tile is consumed exactly once; bounds, shapes and the threadgroup budget are checked, and a refusal names the rule it broke.
One source, two targets
The same .chx kernel becomes CUDA C and Metal, byte-for-byte what the independent Rust implementation writes: 182 emitting cases, 26 refused by both.
Held to an oracle
Goldens come from the Rust, never from this compiler. A CPU interpreter says what a kernel computes; on a Mac the GPU agrees with it to about 1e-6.
Runs a real model
Qwen3.8-27B decodes at 25 tokens a second on a Metal GPU (33 with speculative decoding), with the same tokens and top five as a reference's log-probabilities.
Honest about what is missing
CUDA has run only against a mock driver, speed on the 27B is about half of Ollama's, and attention stops at 1364 positions. Each is in the status section, with its issue.
An authority you can read
The report lists exactly what the compiler may do, and the gate fails if it widens. The one program that calls foreign code says so.
How it works.
A kernel is checked before it is lowered; a refusal never reaches a GPU. The interpreter is the reference for what a kernel computes.
Look at one kernel.
The real source of swiglu.chx, what the compiler writes for each target, and what it says to a program that breaks a rule.
// a = silu(g) * u over a row of n: the gated activation of a SwiGLU MLP.
algo swiglu(n)
in g: f32[1, n]
in u: f32[1, n]
out y: f32[1, n]
{
let gt = load g
let ut = load u
let sg = sigmoid >
let a = gt * sg
store a * ut -> y
}
schedule swiglu for apple-m-series { threads 256 }
schedule swiglu for nvidia-ada { threads 256 }
kernel void swiglu_17408(
device const float* p0_g [[buffer(0)]],
device const float* p1_u [[buffer(1)]],
device float* p2_y [[buffer(2)]],
uint tid [[thread_index_in_threadgroup]],
uint3 tgpos [[threadgroup_position_in_grid]])
{
const uint gid = tgpos.x;
float v2[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v2[k] = float(1.0f / (1.0f + precise::exp(-float(float(p0_g[(uint)(0) * 17408u + (uint)(0) * 1u + ((e))])))));
}
}
float v3[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v3[k] = float(float(float(p0_g[(uint)(0) * 17408u + (uint)(0) * 1u + ((e))])) * float(v2[k]));
}
}
float v4[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v4[k] = float(float(v3[k]) * float(float(p1_u[(uint)(0) * 17408u + (uint)(0) * 1u + ((e / 1u))])));
}
}
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
p2_y[(uint)(0) * 17408u + (uint)(0) * 1u + (e)] = float(float(v4[k]));
}
}
}
extern "C" __global__ void swiglu_17408(
const float* __restrict__ p0_g,
const float* __restrict__ p1_u,
float* __restrict__ p2_y
)
{
const uint tid = threadIdx.x;
const uint gid = blockIdx.x;
float v2[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v2[k] = float(1.0f / (1.0f + expf(-float(float(p0_g[(uint)(0) * 17408u + (uint)(0) * 1u + ((e))])))));
}
}
float v3[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v3[k] = float(float(float(p0_g[(uint)(0) * 17408u + (uint)(0) * 1u + ((e))])) * float(v2[k]));
}
}
float v4[68];
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
v4[k] = float(float(v3[k]) * float(float(p1_u[(uint)(0) * 17408u + (uint)(0) * 1u + ((e / 1u))])));
}
}
for (uint k = 0; k < 68u; ++k) {
const uint e = k * 256u + tid;
if (e < 17408u) {
p2_y[(uint)(0) * 17408u + (uint)(0) * 1u + (e)] = float(float(v4[k]));
}
}
}
$ cancho-gpu check leak.chx n=8
error[leak]: `w.1` is never consumed; drop it, yield it or move it into an op
algo k(n)
in x: f32[1, n]
in w: f32[1, n]
out y: f32[1, n]
{
let a = load x
let b = load w
store sigmoid a -> y
}
schedule k for nvidia-ada { threads 32 }
The Metal and CUDA text is the compiler's actual output for n=17408 (cancho-gpu emit tests/chx/swiglu.chx out n=17408), the kernel function only; the refusal is tests/reject/leak.chx.
A real model on the same kernels.
Qwen3.8-27B (NVFP4, 48 gated-delta layers) reads Ollama's store and runs on a Metal GPU. It is not the fastest way to run this model, and the page says so.
Decode speed, tokens a second, greedy, 128 tokens of a prose continuation, three runs each, one machine (Apple M4 Max, 64 GB), one invocation, with other jobs running on the machine (load average 14 at the start; the runs themselves varied by under 2%) (docs/benchmark.md). Ollama decodes this model with speculative decoding from the model's own draft head; cancho-gpu does too in the speculative row (a chain of two drafts, three rows checked a pass, the same tokens as plain decoding: #26), and is not there yet. MLX-LM uses a different 4-bit quantisation. Prompt processing takes eight tokens at a time: a 49-token prompt in 0.65 s, against 2.2 s a token at a time (one run each).
Correct, not just fast
Three prompts, eight steps each: the same token and top five as the reference's golden log-probabilities, logit gaps within 1e-4.
Small first
Qwen2.5-0.5B gives the same greedy tokens as Hugging Face transformers in float32, every top-5 logit within 1e-4, at about 6 ms a token.
A kernel compiler should be small enough to audit.
| Question | What cancho-gpu does |
|---|---|
| Can the compiler call into C or touch the network? | No. Its report lists args, err_write, fs_read, fs_write, heap and io_write, and says it never touches the network or foreign code. tests/authority.sh fails the gate if that changes. |
| How do I know the output is right? | The Rust emitter is the oracle: goldens are recorded from it, never from this compiler, and a live differential runs the same cases against a build of it. A CPU interpreter checks what a kernel computes, and a device program compares the GPU against it. |
| Does it run on a GPU? | Metal: yes, on Apple Silicon, with the result inside the 2e-2 tolerance of the interpreter. CUDA: only against a mock driver so far, because no machine here has an NVIDIA GPU. The page and the README say which is which. |
| Is it an inference runtime? | A first one. cancho-gpu-run and cancho-gpu-run35 run Qwen2.5-0.5B and Qwen3.8-27B: every layer is a compiled kernel on a Metal GPU. The 0.5B model gives the same greedy tokens as Hugging Face transformers; the 27B (NVFP4 weights from Ollama's store, 48 gated-delta layers) matches a reference implementation's golden log-probabilities to 1e-4 and decodes at about 25 tokens a second (33 with speculative decoding), against about 43 for Ollama in the same benchmark run. It also serves an OpenAI-compatible chat endpoint with streaming (one request at a time). |
Every claim has a measurement, or says it has none.
The oracle is external, so "it works" is a comparison, not an opinion. Where a claim turned out false, the design document was corrected in place.
The design document holds how it is built and what it cost; the device document what is and is not proved on a GPU; the runtime document where each reader differs from its oracle, and the kernels document the context limit.
Where it fits.
Use it for
- Compiling
.chxtile kernels to CUDA C and Metal with a small, auditable toolchain - Checking a kernel on the CPU before a GPU is involved
- Trying a cancho program that drives a GPU through a thin C shim
Not yet
- CUDA on real hardware, Hopper, AMD
- Speed on the 27B model, long contexts (it needs a change to the language), concurrent requests
- Attention over more than 1364 positions (it needs a change to the language)
- Speed: reach and agreement are measured, timing is not
Try it.
You need the cancho compiler at the commit pinned in cancho.toml, and Rust to build it.
git clone https://github.com/alpibrusl/cancho git clone https://github.com/alpibrusl/cancho-gpu && cd cancho-gpu REV=$(sed -n 's/^cancho = "\([0-9a-f]*\)".*/\1/p' cancho.toml) (cd ../cancho && git checkout "$REV" && cargo build --release -p cancho) export CANCHO=$PWD/../cancho/target/release/cancho $CANCHO build mkdir -p out build/cancho-gpu emit tests/chx/softmax.chx out r=16 n=256
Other commands: cancho-gpu check <file.chx> [name=value ...] [--run] and cancho-gpu gguf <file.gguf> [--sum]. scripts/gate.sh is everything CI runs.
Status: alpha
- Built: a model runner for Qwen2 on Metal, the parser, checker, CUDA and Metal emitters, the CPU interpreter, both device paths,
exp/log/softmax, the GGUF and safetensors readers, a BPE tokenizer, a sampler, and rope, kv_append, attention and gate kernels. - Open: split-KV attention (#6), concurrent serving and a prefix cache (#7), AMD (#9), model adapters (#10).
- Unproven: CUDA on a real GPU; the kernels' speed against the best runtimes (about half of Ollama's on the 27B).
cancho-gpu