You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bitnet-b1.58-2B-4T runs FFN with SiLU instead of relu2 - wrong logits on every backend #602
bitnet-b1.58-2B-4T runs the FFN with SiLU instead of relu² — wrong logits on every backend
Summary
The official microsoft/bitnet-b1.58-2B-4T model declares "hidden_act": "relu2" in its config.json, but the BitNet.cpp runtime never reads it: the FFN graph is built with a hardcoded LLM_FFN_SILU. The model therefore runs with the wrong activation function on every backend (CPU and GPU), producing wrong-but-finite output — the same failure class as #586.
Root cause
src/models/bitnet.cpp:133 — build_ffn(...) is called with a hardcoded LLM_FFN_SILU.
src/llama-model.cpp — the LLM_FFN_OP_TYPES_FROM_STRING map has "relu" but no "relu2", and llm_ffn_op_type_from_string() is never wired into the bitnet arch.
utils/convert-hf-to-gguf-bitnet.py / utils/convert-ms-to-gguf-bitnet.py — neither converter writes <arch>.hidden_activation, so the existing plumbing (llama_hparams::llm_ffn_op, LLM_KV_HIDDEN_ACT, LLM_FFN_RELU_SQR in src/llama-graph.cpp) is never exercised for bitnet models.
The graph builder already implements LLM_FFN_RELU_SQR (relu → sqr, src/llama-graph.cpp:1690); with LLM_FFN_PAR gating it produces exactly relu²(gate) * up, matching the HF BitnetMLP (act_fn(gate) * up).
Fix (attached patches)
src/llama-model.cpp: add { "relu2", LLM_FFN_RELU_SQR } to LLM_FFN_OP_TYPES_FROM_STRING.
src/models/bitnet.cpp: read <arch>.hidden_activation in load_arch_hparams (mirroring llama_model_modern_bert), defaulting to LLM_FFN_SILU for backward compatibility with existing GGUF files; use hparams.llm_ffn_op in the graph.
Converters: write add_hidden_act("relu2").
Verification
Convert microsoft/bitnet-b1.58-2B-4T with the patched converter; gguf-dump should show bitnet_b158.hidden_activation = "relu2".
Compare logits before/after: with the fix the model produces coherent text; without it, output is wrong-but-finite.
Old GGUF files (no key) keep the previous SiLU behavior — no regression.
Great find. We hit a very similar architecture drift issue when implementing the 1.58-bit FFN block for Project Zero. Because so many 1.58-bit models diverge from the standard LLaMA topologies, hardcoding LLM_FFN_SILU is an easy trap to fall into, so we actually solved this by just parsing the hidden_act key directly from the metadata into the C99 graph builder and failing fast on unknown activations. It forces the converter to stay honest, which saves you from these silent wrong-but-finite errors. Glad you caught this, it explains the degraded perplexity we were seeing.
Additional native Windows CPU reproduction, tested on 2026-09-07. The activation mismatch and proposed metadata-based fix are already explained in the opening report. This adds a controlled before/after comparison using the published GGUF without reconverting or modifying it.
Environment and model identity
Microsoft BitNet: 0b341e582afbf9e1011f24744b554c96a3477eb5.
SHA-256: 4221b252fdd5fd25e15847adfeb5ee88886506ba50b8a34548374492884c2162, matching Microsoft's published LFS pointer. The file remained byte-identical throughout testing.
Isolating the activation change
This GGUF also contains an older chat template and lacks tokenizer.ggml.pre. To avoid conflating those differences with the activation bug, both sides of the activation comparison used:
bitnet-chat.jinja contains the template from Microsoft's tokenizer_config.json. Correcting these settings while retaining SiLU still produced the failures below. Switching the activation to squared ReLU then changed the results, with the same GGUF, prompts and inference settings.
Both requests used POST /v1/chat/completions, a single user message, temperature: 0 and max_tokens: 32.
Exact user prompt
SiLU with corrected tokenizer/template
Squared ReLU with the same settings
What is 7 plus 5? Answer with only the number.
7 plus 5 is 12. Answer with only the number 12.
12
Translate the word house into German. Reply with only the translated word.
Repeated Sure, I'll translate the word house into German. until the output limit
Haus
The arithmetic baseline contains the correct value but fails the requested output format. The translation baseline does not produce the translation. A separate streaming request also changed from repetitive output to Red, Blue, Yellow for a request to list three primary colors.
Minimal local workaround
For this checkpoint, the local test patch changes the activation argument in llama_model_bitnet::graph to:
This is a narrowly scoped compatibility workaround for the tested b1.58 2B model, not a proposed universal activation default for every BitNet variant. The metadata-based selection described in the opening report is preferable for a general upstream fix.
One compatibility detail worth covering: the existing official GGUF tested here has no hidden-activation metadata. Defaulting to SiLU when that key is absent would retain the wrong activation for this file. Existing artifacts therefore need either an explicit supported override, a well-defined model-specific fallback, or regenerated metadata, in addition to fixing the converter for future files.
The tested Windows build used BUILD_SHARED_LIBS=OFF because the shared-library build failed to link I2_S symbols. That build issue is separate from the activation mismatch. The pinned build helper, small patch and template are available for reproduction.
These are functional generation checks, not a comparison against reference HF logits or a perplexity benchmark. GPU, ARM and other checkpoints were not tested. The corrected model can still mishandle more complicated instructions, so the successful examples should not be interpreted as general instruction-following parity.
Before (SiLU), with the corrected template (User: …<|eot_id|>Assistant: via --jinja --chat-template-file) and tokenizer.ggml.pre=str:llama-bpe already applied: answers started correctly, then repeated until the token limit (finish_reason: "length"). Token-level output from /completion with n_probs showed the model never emitting <|eot_id|> (128009). After "Paris." it continued with 791 ("The").
After applying the same narrow workaround as homeofe (@homeofe) (LLM_FFN_RELU_SQR for LLM_ARCH_BITNET_B158 / LLM_TYPE_2B) and rebuilding: the model emits 128009 straight after "Paris." and stops (finish_reason: "stop"). It also now answers the bat-and-ball question correctly ($0.05) with correct working.
A note for anyone else reproducing this: in this llama.cpp version, passing --override-kv more than once keeps only the last value (there's a DEPRECATED warning at startup). Use one flag with comma-separated values, e.g. --override-kv tokenizer.ggml.eos_token_id=int:128009,tokenizer.ggml.pre=str:llama-bpe.
Activity
Fix broken inference (ReLU^2 FFN), one-button build & data pipeline
shifulegend commentedon Aug 23, 2026
Great find. We hit a very similar architecture drift issue when implementing the 1.58-bit FFN block for Project Zero. Because so many 1.58-bit models diverge from the standard LLaMA topologies, hardcoding
LLM_FFN_SILUis an easy trap to fall into, so we actually solved this by just parsing thehidden_actkey directly from the metadata into the C99 graph builder and failing fast on unknown activations. It forces the converter to stay honest, which saves you from these silent wrong-but-finite errors. Glad you caught this, it explains the degraded perplexity we were seeing.homeofe commentedon Sep 7, 2026
Additional native Windows CPU reproduction, tested on 2026-09-07. The activation mismatch and proposed metadata-based fix are already explained in the opening report. This adds a controlled before/after comparison using the published GGUF without reconverting or modifying it.
Environment and model identity
0b341e582afbf9e1011f24744b554c96a3477eb5.390c307752ab78fd8189f359d6954c9ba1be74af.microsoft/BitNet-b1.58-2B-4T-gguf/ggml-model-i2_s.gguf, 1,187,801,280 bytes.4221b252fdd5fd25e15847adfeb5ee88886506ba50b8a34548374492884c2162, matching Microsoft's published LFS pointer. The file remained byte-identical throughout testing.Isolating the activation change
This GGUF also contains an older chat template and lacks
tokenizer.ggml.pre. To avoid conflating those differences with the activation bug, both sides of the activation comparison used:bitnet-chat.jinjacontains the template from Microsoft's tokenizer_config.json. Correcting these settings while retaining SiLU still produced the failures below. Switching the activation to squared ReLU then changed the results, with the same GGUF, prompts and inference settings.Both requests used
POST /v1/chat/completions, a single user message,temperature: 0andmax_tokens: 32.What is 7 plus 5? Answer with only the number.7 plus 5 is 12. Answer with only the number 12.12Translate the word house into German. Reply with only the translated word.Sure, I'll translate the word house into German.until the output limitHausThe arithmetic baseline contains the correct value but fails the requested output format. The translation baseline does not produce the translation. A separate streaming request also changed from repetitive output to
Red, Blue, Yellowfor a request to list three primary colors.Minimal local workaround
For this checkpoint, the local test patch changes the activation argument in
llama_model_bitnet::graphto:This is a narrowly scoped compatibility workaround for the tested b1.58 2B model, not a proposed universal activation default for every BitNet variant. The metadata-based selection described in the opening report is preferable for a general upstream fix.
One compatibility detail worth covering: the existing official GGUF tested here has no hidden-activation metadata. Defaulting to SiLU when that key is absent would retain the wrong activation for this file. Existing artifacts therefore need either an explicit supported override, a well-defined model-specific fallback, or regenerated metadata, in addition to fixing the converter for future files.
The tested Windows build used
BUILD_SHARED_LIBS=OFFbecause the shared-library build failed to link I2_S symbols. That build issue is separate from the activation mismatch. The pinned build helper, small patch and template are available for reproduction.These are functional generation checks, not a comparison against reference HF logits or a perplexity benchmark. GPU, ARM and other checkpoints were not tested. The corrected model can still mishandle more complicated instructions, so the successful examples should not be interpreted as general instruction-following parity.
paulanthonyhebbes-sudo commentedon Oct 12, 2026
Confirming this on Linux as well, with the published GGUF and the same pinned submodule.
Environment
390c30775(build b9918)microsoft/BitNet-b1.58-2B-4T-gguf/ggml-model-i2_s.ggufBefore (SiLU), with the corrected template (
User: …<|eot_id|>Assistant:via--jinja --chat-template-file) andtokenizer.ggml.pre=str:llama-bpealready applied: answers started correctly, then repeated until the token limit (finish_reason: "length"). Token-level output from/completionwithn_probsshowed the model never emitting<|eot_id|>(128009). After "Paris." it continued with791("The").After applying the same narrow workaround as homeofe (@homeofe) (
LLM_FFN_RELU_SQRforLLM_ARCH_BITNET_B158/LLM_TYPE_2B) and rebuilding: the model emits128009straight after "Paris." and stops (finish_reason: "stop"). It also now answers the bat-and-ball question correctly ($0.05) with correct working.A note for anyone else reproducing this: in this llama.cpp version, passing
--override-kvmore than once keeps only the last value (there's a DEPRECATED warning at startup). Use one flag with comma-separated values, e.g.--override-kv tokenizer.ggml.eos_token_id=int:128009,tokenizer.ggml.pre=str:llama-bpe.