// solo build · in progress
Damascus
I'm building a from-scratch LLM inference engine for Apple M1 - every line by hand in C++20 + Metal. The bet I'm chasing: a hand-rolled AMX prefill path that beats llama.cpp where the M1 has real headroom.
Decode is capped. Prefill has headroom no public runtime touches.
On an M1, decode is bandwidth-bound - ~35 tok/s on 3B Q4 is roughly the ceiling no matter how clever the kernel. Prefill is compute-bound and underused: the GPU and the AMX coprocessor can run in parallel, and AMX's opcodes are undocumented, so nobody exploits them.
- -one token at a time - GEMV-heavy
- -limited by weight streaming from memory
- -clever kernels reach the ceiling, never move it
- -C / D targets: reach decode parity with llama.cpp
- -whole prompt at once - GEMM-heavy, dense matmuls
- -GPU + AMX coprocessor run simultaneously
- -fused dequant→AMX never materializes FP16 weights
- -E target: +30%, 10-run medians, non-overlapping p95
> AMX is driven via raw .word-encoded instructions - bootstrapped from cblas_sgemm (Accelerate already uses AMX) to get the through-the-framework number, then hand-rolled to beat it.
Nine compartments.
No kernel gets optimized before it matches its reference within tolerance.
One pool of memory. Two engines reading it at once.
On Apple Silicon the CPU and GPU share physical memory, so a shared-storage MTLBuffer needs no copy - and the same memory is what AMX reads. That is the basis of the GPU+AMX hybrid: attention on the GPU, the dense FFN on AMX, coordinated through one buffer with no transfers.
SIMD-friendly, runs as Metal compute shaders
outer-product-dense - what the matrix unit wants
no copies between them; the hardware agrees
Token in, logits out - 28 layers of the same graph.
Prefill runs this exact graph over every prompt position at once (matrices, not vectors), filling the KV cache so decode can begin. Prefill is where AMX earns its keep.
prefill
whole prompt at once. GEMM-heavy, compute-bound - the target for the fused dequant→AMX kernel.
one token at a time. GEMV-heavy, bandwidth-bound - reads and writes the KV cache each step.
Five milestones. Every speed number proven, never a lucky run.
Same machine, model, quant, prompt, context length and thermal state - reported with variance. Baselines are measured before the first kernel is written.
`--device-info` runs and prints the device
Logits match HF same-weights oracle - per-layer cosine 1.0, exact top-1 (CPU path)
Greedy decode reads as fluent, sane English
≥ 0.5× llama.cpp decode throughput
≥ 0.9× llama.cpp decode, reproducibly
≥ 1.30× llama.cpp prefill - 10-run medians, non-overlapping p95
Llama 3.2 3B Instruct · Q4_K_M
Big enough that prefill matmuls are real work for AMX, small enough to iterate on a 16 GB M1. Every number is read from the GGUF and asserted - never hardcoded.