Skip to content

fix: I2_S ARM kernel layout + squared-ReLU FFN - passes the gate on the official 2B4T GGUF (#3) [stacked on #2 PR] - #7

Merged
eejd merged 10 commits into
mainfrom
fix/i2s-arm-kernel-relu2
Nov 1, 2026
Merged

eejd merged 10 commits into
mainfrom
fix/i2s-arm-kernel-relu2

Conversation

@eejd

@eejd eejd commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Draft candidate for #3; stacked on fix/macos-arm64-build (the first 5 commits are that PR). Only the last five commits are new.

  • ggml-cpu: I2_S NEON kernel for the canonical layout – patches/llama.cpp/0002, the quants.c hunk of ggml-cpu: fix ARM i2_s dot product producing constant output isHuangXin/llama.cpp#6 verbatim. The pinned submodule used a scalar fallback on ARM that (a) decodes weights sequentially instead of the 128/32 interleaved layout, (b) maps codes to −1/0/+1 although callers subtract the activation sum, (c) implements only the by==0 calling convention so prompt-processing GEMM leaves most outputs unwritten.
  • bitnet-b1.58: squared-ReLU FFN – patches/llama.cpp/0003; hidden_act of 2B4T is relu2, the graph hard-codes SiLU ([Bug]: BitNet FFN uses SILU instead of ReLU² — perplexity 99.8 vs 17.1 on every CPU backend microsoft/BitNet#588/bitnet-b1.58-2B-4T runs FFN with SiLU instead of relu2 - wrong logits on every backend microsoft/BitNet#602). Selecting on arch is not a complete design (1bitLLM checkpoints share the arch but use SiLU); the config-driven key of feat: support config-based FFN activation for BitNet isHuangXin/llama.cpp#4 is the proper fix.
  • ggml: llama-quantize I2_S emits the canonical layout – patches/llama.cpp/0004. llama-quantize … I2_S (what setup_env.py calls) was invalid ftype 'I2_S', and quantize_i2_s wrote the sequential layout. Now: I2_S ftype registered; canonical 128/32 layout; one call per tensor (the scale is per tensor, stored after the packed data, so no row chunking/threads); the 28 unused trailer bytes are zeroed; ggml_validate_row_data accepts I2_S.
  • llama-vocab: assume llama-bpe for the official bitnet-b1.58 GGUF – patches/llama.cpp/0005. The official GGUF has the Llama-3 BPE vocabulary but no tokenizer.ggml.pre, so llama.cpp used the generic default regexes and split e.g. (n):\n differently from Llama-3 (def fibonacci(n):\n → ids 7,77,1680,198 instead of 1471,997). The fallback applies only when arch is bitnet-b1.58, the vocab has 128256 tokens and 280147 merges (merge-table sha identical to Llama-3.2-1B's) and the key is missing; any other file keeps the old warning (checked with a Llama-3.2 GGUF whose key was blanked). The converter already writes llama-bpe for this tokenizer (its check-hash equals Meta-Llama-3's; verified on the HF tokenizer), so only existing files needed this.
  • docs.

Results (Apple M6, -ngl 0, official ggml-model-i2_s.gguf vs mlx_lm float32 mlx-community/bitnet-b1.58-2B-4T, gate from the test/i2s-correctness-gate PR)

build gate teacher-forced (prompts) perplexity gate text (ref 7.1532)
fix/macos-arm64-build (unpatched kernels) FAIL NaN/Inf in 10.2 M logits NaN
+ 0002 only (llama.cpp#6 or #2) FAIL top-1 0.600, mean KL 1.081 43.9906
+ 0002 + 0003 PASS (needs --override-pre llama-bpe) top-1 1.000, mean KL 0.0026, max KL 0.038, min cos 0.996 7.1785 (+0.35 %)
+ 0002..0005 (this branch), unmodified official GGUF, no override PASS, tokenizer parity 5/5 same same

The capital of France is → Paris. Paris is a city that is known for its rich history, culture, (identical to the reference); 3 of 5 greedy continuations are identical over 16 tokens, the other two diverge at reference near-ties (margins 0.028 and 0.174, threshold 0.464). Same result with 1, 4, 8 threads, and with a build without dotprod/i8mm (mean KL 0.0021, ppl 7.1780).

Full run at this head (clean configure + build with the default recipe, patches 0001–0005): kernel tests (linked libggml-cpu) 58 / 58; gate unit tests 24 / 24; synthetic gate PASS; real-model gate PASS with and without the pre-tokenizer override; llama-quantize … I2_S on the synthetic f32 model: all 14 tensors byte-identical to the canonical packing at default/1/4 threads, gate PASS on the quantized file.

Known gaps – not fixed here

  • llama-quantize … I2_S was verified on the synthetic model only; no HF master weights of the real model were on disk, so a locally converted 2B4T GGUF has not been run through the gate. quantize_i2_s keeps the BitNet convention (scale = first non-zero |w|), so inputs must already be ternary × scale. Rows must be divisible by 128 (asserted).
  • 0003 selects squared-ReLU by arch, which is wrong for 1bitLLM checkpoints (SiLU).
  • The --mad variants of the kernel test (src/ggml-bitnet-mad.cpp, not compiled by the CMake build) still fail 0 / 29: dead code with the old kernels.
  • One model, one host, one gate text; the real-model tolerances were not calibrated before the run (see tolerances.json history).

Files

  • CMakeLists.txt, cmake/ApplyLlamaPatches.cmake, docs/macos-arm64.md
  • patches/llama.cpp/0001 (src1_cont), 0002 (I2_S NEON kernel), 0003 (relu²), 0004 (llama-quantize I2_S), 0005 (llama-bpe fallback)

eejd added 4 commits October 31, 2026 13:05
ggml.c (libggml-base) references quantize_i2_s and dequantize_row_i2_s,
which live in ggml-cpu/quants.c (libggml-cpu). ld64 rejects undefined
symbols in a shared library, so the default shared build fails at
libggml-base with:

  Undefined symbols for architecture arm64:
    "_dequantize_row_i2_s", referenced from: _type_traits in ggml.c.o
    "_quantize_i2_s", referenced from: _ggml_quantize_chunk in ggml.c.o

Default BUILD_SHARED_LIBS to OFF on Apple when the user did not set it.

Refs microsoft#682, microsoft#641, microsoft#635 (reproduced on Apple M6 /
Apple clang 21.0.0, see #2).
…d_mul_mat

ggml-cpu.c #undefs GGML_USE_LLAMAFILE when __ARM_FEATURE_MATMUL_INT8 or
__ARM_FEATURE_SVE is defined, which compiles out the only declaration of
src1_cont. The I2_S GEMV/GEMM fast path uses it unconditionally, so every
arm64 CPU with i8mm (M2 and later, Graviton3, ...) fails with:

  ggml-cpu.c:1495:40: error: use of undeclared identifier 'src1_cont'

The submodule (isHuangXin/llama.cpp) is not ours to modify, so carry the
fix as patches/llama.cpp/0001-*.patch and apply it at configure time with
cmake/ApplyLlamaPatches.cmake (idempotent; BITNET_APPLY_LLAMA_PATCHES=OFF
to disable). The same one-line change is in isHuangXin/llama.cpp#1 and #6.

Refs microsoft#618, microsoft#682, microsoft#641 (reproduced on Apple M6 / Apple
clang 21.0.0, see #2).
…S backend)

bitnet.cpp's I2_S/TL1 kernels exist only for the CPU backend, but on macOS
ggml also builds the Metal and Accelerate (BLAS) backends, and both
interfere with I2_S models even when the user passes -ngl 0:

* Metal built, BLAS not: creating the llama_context aborts in
  ggml_backend_sched_split_graph:
    ggml.c:1789: GGML_ASSERT(view_src == NULL || data_size == 0 ||
      data_size + view_offs <= ggml_nbytes(view_src)) failed
* BLAS built: ggml_backend_blas_device_supports_op accepts MUL_MAT whose
  src0 has a to_float() (I2_S has one) once ne0, ne1 and ne10 are >= 32, so
  every prompt batch of >= 32 tokens is routed to ggml_backend_blas_graph_compute
  and the process dies with SIGSEGV (backtrace in the evidence log).

Default GGML_METAL and GGML_BLAS to OFF on Apple unless the user sets them.
Measured on an Apple M6 with a synthetic I2_S model: Metal+BLAS (old default)
crashes for >= 31-token batches; Metal OFF + BLAS OFF runs 1..300 tokens at
1 and 4 threads.

Refs microsoft#682 (SIGSEGV during prompt processing), microsoft#641 (Metal
crash on I2_S), #2
eejd added 4 commits October 31, 2026 13:51
Metal+BLAS: SIGSEGV in ggml_backend_blas_graph_compute (>= 31 tokens); Metal only: GGML_ASSERT at context creation;
BLAS only: no crash, but I2_S batches >= 32 tokens return garbage (perplexity 4.4e27 vs 3.7e5 on the synthetic model with
the corrected kernels). The previous docs text implied a SIGSEGV for every BLAS build.

Refs #2
…ling conventions

Carries the quants.c hunk of isHuangXin/llama.cpp#6 (Scottcjn:fix-arm-i2s-dotprod) verbatim as
patches/llama.cpp/0002. On ARM the pinned submodule used the scalar fallback of ggml_vec_dot_i2_i8_s,
which (a) decodes weights sequentially (byte i/4, field i%4) instead of the interleaved 128-weight /
32-byte layout used by dequantize_row_i2_s and the AVX2 kernel, (b) maps codes to -1/0/1 although the
callers subtract the activation sum (double correction), and (c) implements only the by==0 calling
convention, so ggml_gemm_i2_i8_s (prompt processing, >= 4 columns) leaves most of its output
uninitialised (1e25..1e35 and NaN in the L2 test).

Measured on Apple M6 (kernel tests of test/i2s-correctness-gate): 3 passed / 55 failed before,
55 passed / 3 failed after (the 3 are quantize_i2_s layout, untouched by this patch). L1 gemv/gemm
and L2 mul_mat are exact against an independent reference (rel. error ~1e-7).

Refs #3, microsoft#585, microsoft#598, microsoft#600, microsoft#610, microsoft#641
The shared bitnet graph hard-codes LLM_FFN_SILU. BitNet b1.58 2B4T's config.json has hidden_act = relu2.
Measured on Apple M6 with the official ggml-model-i2_s.gguf against mlx_lm's bitnet model (float32), on a
fixed 199-token text: with the I2_S kernel fixed but SiLU, perplexity is 43.99 vs 7.15 (mean KL 1.08 nats);
with relu2 it is 7.1785 vs 7.1532 (+0.35 %, mean KL 0.0026).

LIMITATION: selecting on arch == bitnet-b1.58 is not a complete design; 1bitLLM checkpoints convert to the
same arch and use SiLU. The config-driven key proposed in isHuangXin/llama.cpp#4 (read hidden_activation
from the GGUF) is the proper fix and needs the converter to write it; this patch is the minimal change that
makes the official 2B4T GGUF correct.

Refs #3, microsoft#588, microsoft#602
eejd added 2 commits November 1, 2026 15:20
quantize_i2_s packed sequentially while dequantize_row_i2_s and the kernels read the
128-weight/32-byte interleaved layout; llama-quantize had no I2_S ftype (invalid ftype 'I2_S').
Also: one call per tensor (the scale is per tensor, after the packed data), zeroed 32-byte
trailer, validate_row_data accepts I2_S.
Refs #3. Kernel tests: 58/58; 14/14 synthetic tensors byte-identical to the
canonical packing at default, 1 and 4 threads; gate PASS on the quantized file.
…out tokenizer.ggml.pre (patch 0005)

Guarded: arch bitnet-b1.58, 128256 tokens, 280147 merges (merge table identical to Llama-3.2's),
no tokenizer.ggml.pre. Any other file keeps the 'missing pre-tokenizer' warning.
Gate on the unmodified official GGUF: tokenizer parity 5/5 (was 3/5), GATE PASS without --override-pre.
Refs #3.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant