I2_S produces incorrect output on aarch64 · Issue #598 · microsoft/BitNet · GitHub
Skip to content

I2_S produces incorrect output on aarch64 #598

Open

Description

I2_S inference is broken on aarch64. The model loads and runs, but output is nonsense — it does not crash, so it looks like a bad model rather than a broken kernel.

On an Orange Pi 5 Plus (RK3588, Debian 12, GCC 12.2) with microsoft/BitNet-b1.58-2B-4T's official ggml-model-i2_s.gguf:

$ llama-cli -m ggml-model-i2_s.gguf -p "The capital of France is" -n 12 --temp 0
> The capital of France is  ????????????????
[ Prompt: 0.7 t/s | Generation: 0.7 t/s ]

The same file on x86-64 gives The capital of France is Paris. at ~40 t/s. Model md5 verified identical on both machines.

Three separate bugs, all in 3rdparty/llama.cpp code paths that x86 never compiles:

  1. QK_I2_S is 128 under AVX2 but 64 under __ARM_NEON, in both quants.c and ggml-cpu-i2s.c. It is the on-disk block size, so it must match the file format on every architecture.
  2. The scalar vec_dot fallback decodes the block-interleaved weight layout sequentially.
  3. ggml_gemm_i2_i8_s's ACT_PARALLEL branch inverts ggml_vec_dot_i2_i8_s's nrc semantics, corrupting prefill.

There is also no NEON path for I2_S at all — aarch64 unpacks one 2-bit weight at a time.

Fixes in isHuangXin/llama.cpp#2, against the pinned 3rdparty/llama.cpp submodule. After them, perplexity on aarch64 matches x86 to 0.161% (74.0952 vs 73.9758, same model and corpus), and kernel output is bit-identical for both GEMV and GEMM.

This may be the same root cause as #55.

Activity

  1. matheus-c-feitosa commented on Oct 7, 2026

    @matheus-c-feitosa

    Independent repro on Neoverse-N1 (2 cores, NEON + dotprod, no i8mm), Ubuntu 24.04, clang 18.1.3 (you used GCC 12.2), same GGUF (ggml-model-i2_s.gguf, sha256 4221b252…2c162, matches HF LFS):

    • BitNet 0b341e5 / llama.cpp submodule 390c307: ???????????? at ~0.8 tok/s, both llama-cli and llama-completion -no-cnv.
    • With Fix I2_S inference on aarch64 and add NEON dot-product path isHuangXin/llama.cpp#2 (8000be4): coherent output, ~13 tok/s gen / ~24 tok/s prompt, same peak RSS (1.54 GB). Chat mode answers "Paris"; raw completion gives "a small city, and the capital of France is a small" — I can't tell yet whether that is the model or residual numeric error (no x86/BF16 reference on my side yet).
    • Only the NEON+dotprod path was exercised; the scalar fallback was not.
    • The loader still warns missing pre-tokenizer type … GENERATION QUALITY WILL BE DEGRADED and the header prints ftype: Q1_0 for this I2_S file, with and without the PR.

    Happy to run a PPL/KL comparison against the BF16 weights if useful.

  2. matheus-c-feitosa commented on Oct 9, 2026

    @matheus-c-feitosa

    Follow-up on my question above about "a small city": it isn't the model. On the same tokens, the HF transformers implementation and ik_llama.cpp both give WikiText-2 PPL ~18.8 and continue "The capital of France is" with " Paris. Paris is a city…", while bitnet.cpp (x86, and aarch64 with the fix) gives PPL ~113 and " a small city…". Details: #639

  3. stevie101 commented on Oct 12, 2026

    @stevie101

    Tested the fixes from isHuangXin/llama.cpp#2 on Apple Silicon. They fix the
    garbage output but quality still doesn't match a reference run.

    Hardware: Apple M5 Max, macOS [26.5.2], Apple clang 21.0.0
    BitNet: main, llama.cpp submodule at 8000be462 (PR branch)
    Model: official microsoft/BitNet-b1.58-2B-4T-gguf ggml-model-i2_s.gguf

    Build notes:

    llama.cpp results (custom chat template matching the GGUF format):

    • Before the PR: "!!!!" / random tokens
    • After the PR: "The capital of France is Paris." but no end-of-turn; output loops
    • "What are the steps to make a cup of tea?" drifts within ~4 steps and loops,
      even with fresh context, --temp 0.3 and repeat penalty

    Reference, same machine, microsoft/bitnet-b1.58-2B-4T-bf16 via transformers
    5.19 on MPS, greedy:

    • France: "The capital of France is Paris." and stops
    • Tea: coherent 5-step answer (choose tea, boil water with temps, prepare,
      pour, steep 3-5 minutes)

    So the remaining gap looks like accumulated numerical error in the aarch64
    I2_S path rather than the model. Speed: ~120 t/s prompt, ~75 t/s generation,
    8 threads. Happy to test further patches.

  4. kiljoy001 commented on Oct 13, 2026

    @kiljoy001
    Author

    I will look into this and see if I can improve it.

  5. matheus-c-feitosa commented on Oct 13, 2026

    @matheus-c-feitosa

    mclaren (@stevie101) before digging into the aarch64 kernel, it's worth applying the activation fix from #588. isHuangXin/llama.cpp#2 fixes the layout, but the graph still uses SiLU where BitNet b1.58 uses ReLU², and that alone wrecks the output quality. The loops without an end of turn you describe match what #602 reports with SiLU: the model never emits <|eot_id|>; with ReLU² it stops.

    On my aarch64 machine (Neoverse-N1, clang 18), with the same official GGUF, WikiText-2 8 x 512, scored on the same token IDs as an HF transformers reference (PPL 18.79):

    build PPL
    main + llama.cpp#2 113.26
    + LLM_FFN_SILU -> LLM_FFN_RELU_SQR (src/models/bitnet.cpp) 18.84
    + #605 instead, with the runtime key (*) 18.84

    (*) --override-kv bitnet-b1.58.hidden_activation=str:relu2. #605 keeps SiLU for files without the key, so by design it does not break the 1bitLLM models that use SiLU (not tested here).

    With either fix, greedy text matches the reference ("The capital of France is Paris. Paris is a city..."), so on this CPU the
    remaining gap after llama.cpp#2 was the activation. Your M5 takes the i8mm path (#618), which I could not test.

    Two more things from #602 that matter for chat: the GGUF has no pre-tokenizer and an outdated chat template. Put all keys in one comma-separated flag (repeating --override-kv prints a deprecation warning), e.g.:

    --override-kv bitnet-b1.58.hidden_activation=str:relu2,\
    tokenizer.ggml.pre=str:llama-bpe
    

    Scott Guyton (@kiljoy001) this may save you some digging. Tables, method and scripts to reproduce:
    https://github.com/matheus-c-feitosa/lowbit-atlas

    Analysis assisted by Claude Code; runs were done on my machines and all numbers checked by me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions