File size: 18,836 Bytes
b4adf81 b039daa 56c1df5 a66a9c7 56c1df5 a66a9c7 56c1df5 a66a9c7 b039daa 56c1df5 a66a9c7 b039daa a66a9c7 b039daa 56c1df5 b039daa a66a9c7 b039daa a66a9c7 56c1df5 a66a9c7 56c1df5 a66a9c7 56c1df5 a66a9c7 56c1df5 a66a9c7 56c1df5 a66a9c7 b039daa a66a9c7 b039daa a66a9c7 b039daa a66a9c7 56c1df5 a66a9c7 56c1df5 a66a9c7 b039daa a66a9c7 56c1df5 a66a9c7 56c1df5 a66a9c7 b039daa a66a9c7 b039daa a66a9c7 56c1df5 a66a9c7 56c1df5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | ---
license: gpl-3.0
language:
- en
library_name: llama.cpp
pretty_name: OldIron
tags:
- llama.cpp
- gguf
- cpu-inference
- inference-engine
- quantization
- avx
- avx1
- sandy-bridge
- hp-z820
- xeon
- cuda
pipeline_tag: text-generation
---
# OldIron
An inference engine plus **one named weight file**, built for hardware that current engines have
written off. OldIron is a product line: each variant names the *lowest* machine it must serve, and nothing in it
is allowed to be slower than that floor needs. The weights are third-party and are not shipped here β see
*The weights* below.
| variant | lowest supported CPU | lowest supported GPU | status |
|---|---|---|---|
| **OldIron-AVX** | Xeon E5-2665 (Sandy Bridge-EP, **AVX1**: no AVX2/FMA/F16C/BMI2) | RTX 3060 8 GB (**sm_86**) | built, staged, **measured on two HP Z820s**; CPU kernel work finished (+13.5% at fixed format, +6.4% net) |
| OldIron-Core2 (planned) | SSE4.1/SSSE3 (2007) | none | not started |
## Results, plainly
**One measured step, and no more.** The AVX1 kernel work shipped and it is worth **+13.5% at fixed format and
+6.4% net** (7.45 β 7.86-7.93 t/s). Every other CPU-lane lever was measured and rejected. The "~3x" projection in
the first version of this README **did not survive the engine tests and is retracted below** β the large multiple
on this hardware is on the GPU lane, not in the matvec.
All numbers measured on an HP Z820 (2Γ Xeon E5-2665, 32 GB DDR3, RTX 5060 Ti) with the 35B-A3B weight file named in
each run, coherent output throughout, `nice -n 19`, box verified idle between arms.
### CPU lane β shipped state
| what | measured | note |
|---|---|---|
| stock engine, IQ3_S 3.66 bpw | **7.45 Β± 0.04 t/s** | `-ngl 0 --no-op-offload -t 24`, the best prior config |
| stock engine, Q4_0 4.50 bpw | 6.99 Β± 0.47 t/s | the wider format is *slower* on stock kernels (23% more bytes) |
| **shipped W3 AVX1 kernel, Q4_0** | **7.86 Β± 0.08 t/s** (bench) Β· 7.93 Β± 0.02 (harness) | **+13.5% vs stock Q4_0, +6.4% net over stock IQ3_S**; coherent |
| prefill pp128 / pp512 / pp4096 | 29.70 / 29.9 / 28.0 t/s | 8Γ the context costs 6% per token |
| W1 GEMV kernel alone | 4.4 GMAC/s/core from DRAM; 8.9 / 18.1 L1-resident (1 / 2 rows) | exact-integer vs the scalar reference, AVX1-clean |
| gcc vs clang 18 vs icx 2026 | gcc best; clang β9%, icx β15.5% | compiler lever closed |
The trade the kernel buys: the Q4_0 weight file is ~19 GiB against 15.7 GiB for IQ3_S. It still fits a 32 GB box,
but it is a size regression in exchange for the speed.
### Retractions β what this README claimed, and what the tests returned
| claim in the first version | measured result |
|---|---|
| "projection when W3 lands ~20-24 t/s, about 3x today" | **retracted.** W3 landed and bought 13.5% at fixed format. The engine moves ~28 GB/s where the pure kernel reaches 44 GB/s, so closing that gap entirely is **~1.35Γ end-to-end** on a 77% matvec share β not 3Γ. The kernel path is close to exhausted |
| "NUMA interleave, one flag, +59%" | **0% in the engine** β 3.13 vs 3.13 t/s, two interleaved rounds, on a copy materialised under the interleave policy. `numastat` also shows the page cache already split 50/50 across nodes. The +59% was a 64 MB streaming microbenchmark; the engine's matvec is not purely bandwidth-bound, so it did not transfer |
| "11.96 GB/s = 16% of the 74.9 GB/s ceiling, so 3-5Γ headroom" | the 74.9 GB/s figure is a **sequential** streaming measurement that this access pattern never reaches. Like-for-like the engine is at 28 GB/s against the kernel's 44 GB/s β 1.6Γ, worth ~1.35Γ end-to-end |
| lm_head Q6_K β Q4_0 (417 MB/token on the old path) | **0%** β 7.83 vs 7.93 t/s, inside noise |
### Also measured and rejected (detail in [`kernels/RESULTS.md`](kernels/RESULTS.md))
- **hoisted-correction GEMM: β40% in situ** (7.93 β 4.67 t/s) despite +113% on the isolated matrix β the engine
makes ~1.9M small `vec_dot` calls per token, so the per-call precompute is never amortised. Reverted.
- **4-row GEMM batching:** inseparable from the hoist that broke it; not shipped.
- **speculative decoding:** 7.3 β 6.8 t/s (**β7%**) with a 0.8B draft on this thinking-heavy output.
- **NUMA chunk granularity** (dropping the `ggml_is_numa()` term): prefill flat, **decode β7%**. Reverted.
- **K-quant tiled GEMM instead of Q4_0 + `vec_dot`:** the tiled path is **44% slower at prefill** on AVX1
(19.00/15.74 t/s vs 29.9/28.0), because it compiles its generic path without AVX2/VNNI. Q4_0 + the W3 kernel
wins on decode *and* prefill.
- **CONCAT "10% of prefill":** a profiler artifact β it was the previous node's barrier wait. Parallelising the
copy changed nothing (pp512 29.97 β 29.92).
- **Prefill ceiling:** pp512 is ~2.5 TMAC at 29.9 t/s β 145 GMAC/s against the measured 170 GMAC/s int8 floor,
i.e. **85-90%**, not the 52% an earlier active-parameter estimate assumed.
### Where the multiple actually is: the GPU lane
The CPU lane is at its ISA ceiling. The 16 GB card is not. Best long-context split on the 35B weights, measured at
a real 32k context with a 140 KB prompt (not `llama-bench`'s short context):
| config | prompt | generation | peak VRAM |
|---|---|---|---|
| **`-ngl 99 -ncmoe 10 -t 24 -c 32768`, f16 KV** | **832.9 t/s** | **52.5 t/s** | 15339 MiB |
| same + KV q8_0 | 839.2 | 48.3 (β8%) | 15339 MiB |
| `-ncmoe 12 -t 24` | 731.7 | 51.6 | 14465 MiB (1.4 GB spare) |
| CPU only (fallback) | 29.9 | 7.86 | β |
**6.7Γ the CPU lane's decode and 29Γ its prefill.** Only 508 MiB spare at `-ncmoe 10`, so 32k is the ceiling with
this split; `-ncmoe 12` costs 2% and holds 1.4 GB of headroom. KV quantisation is a net loss here β the hybrid
DeltaNet KV is ~12 KB/token, so q8 mostly adds dequant work.
## What OldIron-AVX is
One binary, one weight file, three lanes:
| lane | how it runs | target
|---|---|---|
| A β CPU | `-ngl 0` | the AVX1 Xeon pair, no GPU at all |
| B β GPU | `-ngl 99` | RTX 5060 Ti 16 GB (sm_120) |
| C β 8 GB budget | `-ngl 99 -ncmoe N` | RTX 3060 8 GB (sm_86): attention on the card, routed experts on the CPU |
### Engine provenance
- source: llama.cpp `2744e48d1` (build 816, 0.7.0-dev), `qwen35moe` architecture support present
- CPU backend: **one variant**, `-march=sandybridge -mno-avx2 -mno-fma -mno-f16c -mno-bmi2` + `-msse4.2 -mavx`
(ggml prints that as `-msse4.2;-mavx` / `GGML_SSE42;GGML_AVX` β on Sandy Bridge that *is* AVX1, not a lower floor)
- `-march=sandybridge` also sets `-mtune=sandybridge`, so the code is scheduled for the Z820 core even though it is
built on a Zen4 host
- CUDA fatbin: **`sm_86` + `sm_120`**, 145 kernels each (RTX 3060 is the supported floor, 5060 Ti the test card)
- `GGML_LLAMAFILE=ON`, `GGML_CPU_REPACK=ON`, `GGML_OPENMP=ON`
- `RUNPATH=$ORIGIN/../lib` on binaries, `$ORIGIN` on libs β the tree is relocatable
### ISA gate (measured, not asserted)
`scripts/isa-audit.sh` on the shipped `libggml-cpu.so`:
```
AVX2-only mnemonics : 0 FMA : 0
int-vector on %ymm : 0 F16C : 0
AVX-512 (%zmm) : 0 AVX1 %ymm ops : 5335
VERDICT: AVX1-clean and using 256-bit width
```
**The audit pattern matters.** A naive grep for "AVX2 opcodes" reports false positives: `vpmulld`, `vpblendvb`,
`vpminsd`/`vpmaxsd` are SSE4.1 instructions whose **VEX.128 form is legal AVX1**, and unanchored patterns like
`v(p[a-z]*|ps[a-z]*)` match the `vps` inside `vdivps`/`vpermilps`. The sound tests are (a) genuinely AVX2-only
mnemonics (`vpermd`, `vpermq`, `vpbroadcast[dwq]`, `vpsllvd`, `vpgather*`, `vpblendd`) and (b) **integer-vector ops
on `%ymm`** β AVX1 has no 256-bit integer arithmetic at all. `vmovdqa/vmovdqu` on `%ymm` are AVX1-legitimate.
### The weights: third-party, and not shipped
`Qwen3.6-35B-A3B` in GGUF β 35B total / 3B active, arch `qwen35moe` (256 experts, 8 routed + 1 shared, 40 layers,
3:1 Gated DeltaNet : full attention, 262k context, 248320 vocab, MTP-trained). Three files appear in these
measurements: the stock `Qwen3.6-35B-A3B-IQ3_M.gguf` (**16.9 GB, 3.52 bits per weight**), an `IQ3_S` mix
(3.66 bpw, 15.73 GiB) used for the lane matrix, and a `Q4_0` re-quant of the same model (4.50 bpw, ~19 GiB)
written only to A/B the AVX1 kernel.
**These weights are not ours, and this project does not include them.** They are third-party GGUF files of a public
model, downloaded and checksum-verified in transit; the `Q4_0` copy is a local re-quant of the same model and is a
test artefact, not a product. What is published here is engine source, kernel source, scripts and measurements β no
weight file is redistributed, and no weight file is claimed as part of the deliverable.
Why this shape of model: decode speed is set by *active* parameters and RAM is the scarce resource, so a 35B/3B
MoE at 3.5-4.5 bits fits a 32 GB box with ~15 GB to spare for a long-context KV cache (20 KB/token β 32k ctx
β 0.67 GB), where a 27B dense at a higher quant would be ~7Γ slower for less quality headroom.
## Measured: HP Z820 (2Γ E5-2665, 16 cores, RTX 5060 Ti)
`Qwen3.6-35B-A3B-IQ3_S` mix, 3.66 bpw, 15.73 GiB, 34.66B params β coherent on every lane
("The capital of France is **Paris**.").
| lane | config | decode | prefill | peak VRAM | verdict |
|---|---|---|---|---|---|
| A β CPU only | `-ngl 0 -nopo 1 -t 16` | 7.25 Β± 0.10 t/s | 28.8 t/s (pp128) | β | works, kernel-bound |
| A β CPU only | `-ngl 0 -nopo 1 -t 24` | **7.56 Β± 0.03 t/s** | 28.5 t/s | β | best CPU arm on stock kernels |
| A β CPU, shipped kernel | Q4_0 + W3, `-t 24` | **7.86 Β± 0.08 t/s** | 29.70 t/s (pp128) | β | what actually ships |
| B β 5060 Ti maximised | `-ngl 99 -ncmoe 4` | **73.3 Β± 1.0 t/s** | **763.7 t/s** (pp512) | 14797 MiB | best short-context GPU arm |
| B β | `-ngl 99 -ncmoe 8` | 50.8 t/s | β | 13345 MiB | |
| C β 3060 8 GB, tight | `-ncmoe 24` | 29.7 t/s | β | 7739 MiB | fits, ~450 MiB margin |
| **C β 3060 8 GB, safe** | **`-ncmoe 32`** | **23.8 t/s** | 63.9 t/s | **4905 MiB** | **recommended: 3 GB headroom for context** |
| C β | `-ncmoe 40` | 19.1 t/s | 52.7 t/s | 1869 MiB | |
| β | `-ngl 99` alone | **OOM** | β | β | 15898 MiB allocation fails |
**"GPU full" does not exist for this weight set on a 16 GB card.** 15.73 GiB of weights + KV + context will not fit
15.8 GiB of VRAM, so the GPU lane is *always* hybrid here: `-ncmoe 4` on a 16 GB card, `-ncmoe 32` on an 8 GB card.
For long context the split that matters is in *Results, plainly* above (`-ncmoe 10`, 52.5 t/s at 32k).
CPU lane effective throughput: ~1.54 GB/token (3B active + the 248k-vocab lm_head) Γ· 132 ms β **11.7 GB/s.** That
is against the **44 GB/s** the pure kernel reaches on this access pattern, not against the box's 74.9 GB/s
sequential streaming figure β see the retractions above for why the difference matters.
### Earlier proxy run (kept for comparison)
`gpt-oss-20b Q4_K_M`, 20.9B total / 3.6B active, 10.81 GiB, measured before the 35B weights finished copying:
| lane | decode | prefill | note |
|---|---|---|---|
| A CPU only, `-t 16` | **8.03 Β± 0.02 t/s** | **23.9 t/s** (pp512, `--no-op-offload`) | coherent ("β¦is Paris") |
| B GPU full (`-ngl 99`) | **142.7 Β± 2.4 t/s** | 297 t/s | coherent |
| C `-ncmoe 16` | 19.7 t/s | β | peak VRAM 4605 MiB |
| C `-ncmoe 24/32` | 13.4β13.9 t/s | β | peak VRAM 1373 MiB |
### Two measurement traps, both found here
**1. `-ngl 0` is not CPU-only when a CUDA backend is present.** Op-offload defaults to ON and moves the batched
matmuls to the GPU. The contaminated arms reported **242β341 t/s of "CPU prefill"** β physically impossible on this
box (it implies ~3 TMAC/s against a ~0.3 TMAC/s ceiling). With `--no-op-offload` the same arm reads **23.9 Β± 0.9
t/s**. Decode was never affected, only prefill. Any CPU-lane prefill number without `--no-op-offload` is a GPU
number wearing a CPU label.
**2. `-fitt` is not a VRAM cap** (see below).
### AVX1 floor vs `-march=native`: the floor costs nothing
Same source (2744e48d1), same model, same flags, built both ways on the Z820 itself:
| build | decode (tg48/128) | prefill (pp512) |
|---|---|---|
| OldIron-AVX floor (`-march=sandybridge`, cross-built on Zen4) | 8.55 / 9.03 t/s | 23.9 Β± 0.9 t/s |
| native (`-march=native`, built on the E5-2665) | 8.74 / 8.83 t/s | ~25 t/s |
Parity within run-to-run spread. Repeated on the 35B weights: floor **7.56 Β± 0.03** (t=24) / 7.25 Β± 0.10 (t=16)
vs native **7.63 Β± 0.05** t/s β ~1% apart. One detached harness pass measured the two back to back:
floor **7.38 Β± 0.02** vs native **7.43 Β± 0.05**: **0.7%**, which is the run-to-run spread itself. Three independent passes of the same arms now agree within ~3-6%, so the floor is confirmed free. The two targets differ only in `-maes` (AES-NI), which no inference kernel
uses, so the cross-built floor is not leaving AVX1 performance on the table and OldIron-AVX can be built anywhere.
(An A/B against the fleet's older `ik_llama-sb-avx1` floor build could not be run: its `llama-cli` exits
`Permission denied` even after `chmod +x`, so that binary is unusable as an arm β unresolved, and not needed while
the same-commit native comparison exists.)
CPU lane effective weight throughput on that run: 3.6B active Γ 4.85 bits β 2.18 GB/token Γ· 125 ms β
**17.5 GB/s.** Measured at 8 / 16 threads the engine moves 24 / **28 GB/s**, against 44 GB/s for the pure kernel on
a sequential 64 MB matrix β within 1.6Γ, which is the honest size of the kernel headroom.
### Open item: `-fitt` is not a safe VRAM cap
`-fitt 7400` produced a **peak of 8515 MiB** and `-fitt 6400` a peak of **9447 MiB** β both above an 8 GB card's
budget. The fit heuristic and `-ncmoe` fight each other, and the peak includes the CUDA context. Lane C must be
pinned with an explicit `-ncmoe` and verified by sampling `nvidia-smi` *during* the run (sampling before/after
reads 7 MiB and proves nothing).
## One binary, both Z820s (the compatibility claim, measured)
Same OldIron-AVX binary, CPU lane, `-ngl 0 -nopo 1`:
| host | CPU | quant | decode | prefill | effective weight throughput |
|---|---|---|---|---|---|
| Z820 #1 | 2Γ E5-2665 @ 2.4 GHz | IQ3_M, 3.66 bpw | 7.45 Β± 0.04 t/s (t=24) | 28.5 t/s | **11.96 GB/s** |
| Z820 #2 | 2Γ E5-2687W @ 3.1 GHz | Q4_K_M, 4.85 bpw | 5.66 Β± 0.02 t/s (t=16) | 40.7 t/s | **12.04 GB/s** |
Normalised for quant, the two boxes move **within 0.7% of the same bytes per second** β decode is DRAM-bound, so the
29% clock advantage buys nothing there, exactly as the memory model predicts. Prefill is compute-bound and *does*
scale with clock (1.43Γ). Coherent on both ("The capital of France is **Paris**.").
That is the compatibility measurement, not an argument: one AVX1 binary covers both boxes with no per-host build, and
the only thing that changed the token rate was the weight file.
## Operational notes (learned the hard way on this build)
- **Run the harness detached, in tmux** β the wifi link to the Z820s drops ssh sessions mid-run. tmux is installed
without root on both Z820s (`scripts/install-tmux-noroot.sh`: `apt-get download` + `dpkg -x` into `~/tmux-local`,
wrapper at `~/bin/tmux`); the build host has it from apt.
- **Under tmux the CLI gets a pty and enters its interactive REPL** after the first turn, hanging until the timeout
instead of exiting. Every non-interactive CLI arm needs `< /dev/null`. `llama-cli` also writes a **0-byte log**
under a tmux pty; capture CLI output over a plain ssh pipe (`llama-bench` is fine under tmux).
- **Never point `cuobjdump` at a symlink and its target together**: given `libggml-cuda.so libggml-cuda.so.0
libggml-cuda.so.0.27.0` it prints a bogus mixed summary (`sm_50β¦sm_101`, 1β2 each) that looks like arch
contamination. On one `readlink -f` path it reports the truth: 145 `sm_120` + 145 `sm_86`.
- **Coherence capture belongs on non-interactive ssh**, not the tmux arm: under the pty the answer text is not
captured even with stdin closed, so the gate is run over plain ssh where it has produced the correct answer
repeatedly.
- **Confirm the box is idle between arms** β a stale `llama-bench` survived an ssh timeout and competed with three
subsequent measurements for 23 minutes (one produced a nonsense 1.31 t/s).
- **CMake does not rebuild on a compile-definition change** β flipping `GGML_CPU_REPACK` rewrote `flags.make` and
left every object in place, producing a fake "repack off" result. Delete the target's `*.o` and touch the sources.
- **Never quote a rate from a profiled run** β `OLDIRON_OPPROF=1` costs ~1.6x (19.0 s of op time for 96 tokens vs
12.3 s), so a profiler run and a clean run are not comparable.
## Acceptance gates (a fast wrong answer is a fail)
1. **ISA** β `scripts/isa-audit.sh` on the shipped libs: 0 above the floor, `%ymm` present.
2. **Fatbin coverage** β `cuobjdump --list-elf` shows `sm_86` and `sm_120`.
3. **Coherence** β fixed prompts through each lane, text compared to a known-good engine. This fleet has shipped a
floored build that answered `The capital of France is!!!!!!!!` at 65 t/s; speed is measured last, on purpose.
4. **Speed** β `llama-bench`, thread sweep on the CPU lane (expect an optimum, not the largest count).
5. **8 GB class** β lane C must peak under ~7.8 GB sampled during the run.
## What is in this repository
Source, scripts and measurements. Not included, deliberately:
- `src/` - the engine is an unmodified llama.cpp worktree at commit `2744e48d1`; clone it from upstream rather than
vendoring it here.
- weights - `Qwen3.6-35B-A3B` and `gpt-oss-20b` GGUFs are third-party downloads (HF), not redistributed.
- build output, staged binaries, kernel test binaries.
```
kernels/ the AVX1 quantised GEMV/GEMM work (source only) and RESULTS.md, the waypoint log
scripts/ build (AVX1 floor + sm_86;120), ISA audit, staging, weight transfer, acceptance matrix
results/ the measured runs
```
## License
GPL-3.0-or-later. See `LICENSE`; the copyright notice and the third-party terms (llama.cpp is MIT and stays MIT)
are in `NOTICE`.
## Layout
```
/home/bob/OldIron/
src/ llama.cpp worktree @ 2744e48d1
build/ the built engine (bin/ + lib/)
stage/ patched-RUNPATH copy for shipping
scripts/
build-oldiron-avx.sh build (AVX1 floor + sm_86;120)
isa-audit.sh the ISA gate (see note above about the pattern)
stage-to-target.sh relocatable copy + load check
fetch-weights.sh parallel-stream weight transfer (link is wifi/WAN here)
acceptance-hp-z820.sh the full matrix on the Z820
```
On a target machine the engine sits at `<root>/engine/{bin,lib}` with the weight file wherever you keep it β nothing
under this repository's tree.
|