OldIron / README.md
rkeyes's picture
Restore the model-card frontmatter (previous commit uploaded the bare README)
b4adf81 verified
|
Raw History Blame Contribute Delete
18.8 kB
---
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.