- OldIron
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 |
| "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)
- 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_dotcalls 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),qwen35moearchitecture 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=sandybridgealso 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=ONRUNPATH=$ORIGIN/../libon binaries,$ORIGINon 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 -xinto~/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-clialso writes a 0-byte log under a tmux pty; capture CLI output over a plain ssh pipe (llama-benchis fine under tmux). - Never point
cuobjdumpat a symlink and its target together: givenlibggml-cuda.so libggml-cuda.so.0 libggml-cuda.so.0.27.0it prints a bogus mixed summary (sm_50β¦sm_101, 1β2 each) that looks like arch contamination. On onereadlink -fpath it reports the truth: 145sm_120+ 145sm_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-benchsurvived 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_REPACKrewroteflags.makeand left every object in place, producing a fake "repack off" result. Delete the target's*.oand touch the sources. - Never quote a rate from a profiled run β
OLDIRON_OPPROF=1costs ~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)
- ISA β
scripts/isa-audit.shon the shipped libs: 0 above the floor,%ymmpresent. - Fatbin coverage β
cuobjdump --list-elfshowssm_86andsm_120. - 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. - Speed β
llama-bench, thread sweep on the CPU lane (expect an optimum, not the largest count). - 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 commit2744e48d1; clone it from upstream rather than vendoring it here.- weights -
Qwen3.6-35B-A3Bandgpt-oss-20bGGUFs 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.