This guide shows how to set up C++ reference implementations for cross-validation with BitNet-rs. BitNet-rs supports dual-backend cross-validation, allowing you to validate against either BitNet.cpp or llama.cpp depending on your model.
BitNet-rs provides comprehensive cross-validation infrastructure to compare Rust inference against official C++ implementations:
Lane A: BitNet-rs vs bitnet.cpp
- Models: microsoft-bitnet-b1.58-2B-4T-gguf (BitNet-specific models)
- Source: microsoft/BitNet
- Libraries:
libbitnet*.so(or.dylibon macOS) - Use Cases: BitNet quantization validation, 1-bit inference parity
Lane B: BitNet-rs vs llama.cpp
- Models: llama-3, llama-2, SmolLM3, and other GGUF-compatible models
- Source: ggml-org/llama.cpp
- Libraries:
libllama*.so,libggml*.so - Use Cases: General GGUF model validation, LLaMA-family inference parity
The crossval-per-token command automatically selects the appropriate backend based on your model path:
- Path contains
"bitnet"or"microsoft/bitnet"→ Uses bitnet.cpp - Path contains
"llama"→ Uses llama.cpp - Default: llama.cpp (conservative fallback)
You can override auto-detection with --cpp-backend bitnet|llama.
# Auto-bootstrap both backends
eval "$(cargo run -p xtask -- setup-cpp-auto --emit=sh)"This command fetches, builds, and configures the appropriate C++ reference implementation and emits environment variable exports for your shell.
The easiest way is to use setup-cpp-auto, which automatically selects and builds the appropriate backend(s):
# Bash/Zsh
eval "$(cargo run -p xtask -- setup-cpp-auto --emit=sh)"
# Fish shell
cargo run -p xtask -- setup-cpp-auto --emit=fish | source
# PowerShell
cargo run -p xtask -- setup-cpp-auto --emit=pwsh | Invoke-ExpressionThis command:
- Detects if you need bitnet.cpp or llama.cpp (or both)
- Fetches and builds the C++ reference(s)
- Emits shell-specific environment variable exports
- Sets
BITNET_CPP_DIRand dynamic loader paths automatically
Pro Tip: The emitted exports are printed to stdout. You can add them to your shell profile to make them permanent:
# Bash/Zsh - add to ~/.bashrc or ~/.zshrc
cargo run -p xtask -- setup-cpp-auto --emit=sh >> ~/.bashrc
# Fish - add to ~/.config/fish/config.fish
cargo run -p xtask -- setup-cpp-auto --emit=fish >> ~/.config/fish/config.fishIf you need fine-grained control, set up backends individually:
For BitNet Models (BitNet.cpp backend):
# Download and build bitnet.cpp with shared library
cargo run -p xtask -- fetch-cpp --backend cpu
# The build completes in ~/.cache/bitnet_cpp by default
# Set environment variables (Linux)
export BITNET_CPP_DIR="$HOME/.cache/bitnet_cpp"
export LD_LIBRARY_PATH="$BITNET_CPP_DIR/build/bin:$LD_LIBRARY_PATH"
# Set environment variables (macOS)
export BITNET_CPP_DIR="$HOME/.cache/bitnet_cpp"
export DYLD_LIBRARY_PATH="$BITNET_CPP_DIR/build/bin:$DYLD_LIBRARY_PATH"
# Set environment variables (Windows PowerShell)
$env:BITNET_CPP_DIR = "$env:USERPROFILE\.cache\bitnet_cpp"
$env:PATH = "$env:BITNET_CPP_DIR\build\bin;$env:PATH"On Windows, the PATH environment variable is used instead of LD_LIBRARY_PATH to locate DLL files. Below are six methods to configure PATH for BitNet-rs C++ libraries.
Temporary PATH update for the current PowerShell session only:
# Set PATH for current session
$env:PATH += ";C:\path\to\bitnet.cpp\build"
$env:PATH += ";C:\path\to\llama.cpp\build\bin\Release"
# Verify DLLs are found
where.exe libbitnet.dll
where.exe llama.dllPros: Quick testing, no system changes Cons: Lost when PowerShell window closes
Update PATH permanently for the current user:
# Get current user PATH
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
# Append new paths (semicolon-separated)
$newPath = "$currentPath;C:\path\to\bitnet.cpp\build;C:\path\to\llama.cpp\build\bin\Release"
# Save to user environment
[Environment]::SetEnvironmentVariable("PATH", $newPath, "User")
# Restart PowerShell to applyPros: Permanent, doesn't require admin, user-specific Cons: Requires PowerShell restart, user-scoped only
Update PATH system-wide (all users):
# Run PowerShell as Administrator
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "Machine")
$newPath = "$currentPath;C:\path\to\bitnet.cpp\build"
[Environment]::SetEnvironmentVariable("PATH", $newPath, "Machine")Pros: System-wide, persists across users Cons: Requires admin rights, affects all users
Temporary PATH update for the current CMD session:
REM Set PATH for current session
set PATH=%PATH%;C:\path\to\bitnet.cpp\build
set PATH=%PATH%;C:\path\to\llama.cpp\build\bin\Release
REM Verify DLLs
where libbitnet.dll
where llama.dllPros: Quick testing Cons: Lost when CMD window closes
Update PATH permanently (user scope):
REM WARNING: setx has 1024-character limit for PATH
setx PATH "%PATH%;C:\path\to\bitnet.cpp\build"
REM Restart CMD to applysetx has a 1024-character PATH limit. If your PATH is long, use Method 2 or 6 instead.
Pros: Permanent Cons: 1024-char limit, user-scoped only, requires CMD restart
Configure PATH via Windows System Properties:
-
Open System Properties:
- Press
Win + Pauseor right-click "This PC" → "Properties" - Click "Advanced system settings"
- Click "Environment Variables..." button
- Press
-
Edit User PATH (current user):
- In "User variables" section, select "Path"
- Click "Edit..."
- Click "New"
- Add:
C:\path\to\bitnet.cpp\build - Click "OK" on all dialogs
-
Edit System PATH (all users, requires admin):
- In "System variables" section, select "Path"
- Click "Edit..." (requires admin rights)
- Click "New"
- Add:
C:\path\to\bitnet.cpp\build - Click "OK" on all dialogs
-
Apply changes:
- Restart any open terminal windows
- Verify with
where libbitnet.dll
Pros: GUI-based, persistent, system or user scope Cons: Requires manual navigation, restart terminals
After configuring PATH, verify DLLs are found:
# PowerShell
where.exe libbitnet.dll
where.exe llama.dll
where.exe ggml.dll
# Command Prompt
where libbitnet.dll
where llama.dllExpected output: Full path to each DLL, e.g., C:\path\to\bitnet.cpp\build\libbitnet.dll
If not found:
- Restart terminal (PATH changes require new sessions)
- Check DLL filenames match:
dir C:\path\to\bitnet.cpp\build\*.dll - Verify no typos in path
Use dumpbin (Visual Studio tool) to check DLL dependencies:
dumpbin /dependents C:\path\to\bitnet.cpp\build\libbitnet.dllOutput shows: Required DLLs (e.g., msvcr140.dll, kernel32.dll)
If missing dependencies: Install Visual C++ Redistributable from Microsoft.
Problem: "The specified module could not be found"
Solution: DLL dependencies missing
- Install Visual C++ Redistributable
- Check dependencies with
dumpbin /dependents
Problem: "where" command not found
Solution: Use full path C:\Windows\System32\where.exe or add System32 to PATH
Problem: PATH not updating after setx
Solution: Restart terminal completely (close all windows, open new one)
Problem: setx fails with "The maximum length of the value is 1024 characters"
Solution: Use Method 2 (PowerShell) or Method 6 (GUI) instead
For LLaMA Models (llama.cpp backend):
If you need separate llama.cpp setup (optional—bitnet.cpp includes llama.cpp):
# Clone and build llama.cpp independently
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DBUILD_SHARED_LIBS=ON
cmake --build build --config Release -j
cd ..
# Set LLAMA_CPP_DIR if using standalone llama.cpp
export LLAMA_CPP_DIR="$(pwd)/llama.cpp"
export LD_LIBRARY_PATH="$LLAMA_CPP_DIR/build/bin:$LD_LIBRARY_PATH" # LinuxMake Permanent: Add the export commands to your shell profile (~/.bashrc, ~/.zshrc, etc.).
Verify that the C++ libraries are discoverable:
# Check that shared libraries exist and are discoverable
# Linux (bitnet.cpp):
ldd ~/.cache/bitnet_cpp/build/bin/libbitnet.so
# Linux (llama.cpp):
ldd ~/.cache/bitnet_cpp/build/bin/libllama.so
# macOS (bitnet.cpp):
otool -L ~/.cache/bitnet_cpp/build/bin/libbitnet.dylib
# macOS (llama.cpp):
otool -L ~/.cache/bitnet_cpp/build/bin/libllama.dylibVerify that xtask can access C++ functionality:
# BitNet model auto-detection (will use bitnet.cpp)
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/microsoft-bitnet-b1.58-2B-4T-gguf/ggml-model-i2_s.gguf \
--tokenizer models/microsoft-bitnet-b1.58-2B-4T-gguf/tokenizer.json \
--prompt "What is 2+2?" \
--max-tokens 1 \
--verbose
# LLaMA model auto-detection (will use llama.cpp)
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/llama-3-8b-instruct.gguf \
--tokenizer models/tokenizer.json \
--prompt "What is 2+2?" \
--max-tokens 1 \
--verboseThe --verbose flag shows backend selection, library detection, and preflight results.
Once setup is complete, you can use the full cross-validation toolkit. The toolkit automatically selects the appropriate C++ backend:
BitNet Model Validation (auto-detects bitnet.cpp):
# Per-token logits divergence detection (BitNet)
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/microsoft-bitnet-b1.58-2B-4T-gguf/ggml-model-i2_s.gguf \
--tokenizer models/microsoft-bitnet-b1.58-2B-4T-gguf/tokenizer.json \
--prompt "What is 2+2?" \
--max-tokens 4 \
--cos-tol 0.999 \
--format jsonLLaMA Model Validation (auto-detects llama.cpp):
# Per-token logits divergence detection (LLaMA)
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/llama-3-8b-instruct.gguf \
--tokenizer models/tokenizer.json \
--prompt "What is the capital of France?" \
--max-tokens 8 \
--cos-tol 0.999 \
--format jsonExplicit Backend Selection (override auto-detection):
# Force bitnet.cpp even for non-BitNet models
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/some-model.gguf \
--tokenizer models/tokenizer.json \
--cpp-backend bitnet \
--prompt "Test" \
--max-tokens 4
# Force llama.cpp even for BitNet models
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/microsoft-bitnet-b1.58-2B-4T-gguf/ggml-model-i2_s.gguf \
--tokenizer models/microsoft-bitnet-b1.58-2B-4T-gguf/tokenizer.json \
--cpp-backend llama \
--prompt "Test" \
--max-tokens 4./scripts/run_crossval_sweep.sh
models/microsoft-bitnet-b1.58-2B-4T-gguf/ggml-model-i2_s.gguf
models/microsoft-bitnet-b1.58-2B-4T-gguf/tokenizer.json
/tmp/crossval-sweep
BITNET_TRACE_DIR=/tmp/rs RUST_LOG=warn BITNET_DETERMINISTIC=1 BITNET_SEED=42
cargo run -p bitnet-cli --features cpu,trace -- run
--model models/microsoft-bitnet-b1.58-2B-4T-gguf/ggml-model-i2_s.gguf
--tokenizer models/microsoft-bitnet-b1.58-2B-4T-gguf/tokenizer.json
--prompt "What is 2+2?" --max-tokens 1 --greedy
python3 scripts/trace_diff.py /tmp/rs /tmp/cpp
## Manual Setup (Advanced)
If you need fine-grained control over the C++ build, you can build manually:
### Building BitNet.cpp
BitNet.cpp is the primary reference for 1-bit models and includes llama.cpp as a dependency:
```bash
# Clone the official reference
git clone https://github.com/microsoft/BitNet
cd BitNet
# Build bitnet.cpp (CPU-only)
cmake -B build -DBUILD_SHARED_LIBS=ON
cmake --build build --config Release -j
# Build with CUDA support
cmake -B build -DBUILD_SHARED_LIBS=ON -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=80;86
cmake --build build --config Release -j
# Set environment variables
export BITNET_CPP_DIR="$(pwd)"
export LD_LIBRARY_PATH="$(pwd)/build/bin:$LD_LIBRARY_PATH" # Linux
export DYLD_LIBRARY_PATH="$(pwd)/build/bin:$DYLD_LIBRARY_PATH" # macOS
Key Points:
- BitNet.cpp is built on top of llama.cpp, so it includes both
libbitnet.soandlibllama.so - Use
-DBUILD_SHARED_LIBS=ONto generate shared libraries - Shared libraries appear in
build/bin/as:- Linux:
libbitnet.so,libllama.so,libggml.so - macOS:
libbitnet.dylib,libllama.dylib,libggml.dylib - Windows:
bitnet.dll,llama.dll,ggml.dll
- Linux:
If you need llama.cpp without bitnet.cpp (for LLaMA-family models only):
# Clone llama.cpp
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
# Build shared library (CPU-only)
cmake -B build -DBUILD_SHARED_LIBS=ON
cmake --build build --config Release -j
# Build with CUDA support
cmake -B build -DBUILD_SHARED_LIBS=ON -DGGML_CUDA=ON
cmake --build build --config Release -j
# Set environment variables for standalone llama.cpp
export LLAMA_CPP_DIR="$(pwd)"
export LD_LIBRARY_PATH="$(pwd)/build/bin:$LD_LIBRARY_PATH" # Linux
export DYLD_LIBRARY_PATH="$(pwd)/build/bin:$DYLD_LIBRARY_PATH" # macOSNote: BitNet-rs --cpp-backend llama will look for libllama.so in $BITNET_CPP_DIR/build/bin. If using standalone llama.cpp, either set BITNET_CPP_DIR to your llama.cpp directory or use LLAMA_CPP_DIR and ensure the loader can find the libraries.
Error: "libbitnet.so: cannot open shared object file" or "libllama.so: cannot open shared object file"
Cause: The dynamic loader can't find the C++ libraries.
Fix:
# Verify the library exists (BitNet)
ls -lh ~/.cache/bitnet_cpp/build/bin/libbitnet.*
# Verify the library exists (LLaMA)
ls -lh ~/.cache/bitnet_cpp/build/bin/libllama.*
# Set the dynamic loader path (Linux)
export BITNET_CPP_DIR="$HOME/.cache/bitnet_cpp"
export LD_LIBRARY_PATH="$BITNET_CPP_DIR/build/bin:$LD_LIBRARY_PATH"
# Set the dynamic loader path (macOS)
export BITNET_CPP_DIR="$HOME/.cache/bitnet_cpp"
export DYLD_LIBRARY_PATH="$BITNET_CPP_DIR/build/bin:$DYLD_LIBRARY_PATH"
# Make permanent (add to ~/.bashrc or ~/.zshrc)
echo 'export LD_LIBRARY_PATH="$HOME/.cache/bitnet_cpp/build/bin:$LD_LIBRARY_PATH"' >> ~/.bashrc
source ~/.bashrc
# Verify ldd/otool can find the libraries
# Linux:
ldd ~/.cache/bitnet_cpp/build/bin/libbitnet.so | grep "not found"
# macOS:
otool -L ~/.cache/bitnet_cpp/build/bin/libbitnet.dylib | grep "not found"Cause: BitNet.cpp wasn't built or the build directory is missing.
Fix:
# Rebuild bitnet.cpp
cargo run -p xtask -- fetch-cpp --backend cpu --force
# Verify both backends are available
ls -lh ~/.cache/bitnet_cpp/build/bin/lib*.so # Linux
# Output should show: libbitnet.so, libllama.so, libggml.soCause: llama.cpp wasn't built or is missing from the build directory.
Fix:
# If you built bitnet.cpp, it includes llama.cpp by default
# Verify the library exists:
ls -lh ~/.cache/bitnet_cpp/build/bin/libllama.so # Linux
# If using standalone llama.cpp:
export BITNET_CPP_DIR="/path/to/llama.cpp"
export LD_LIBRARY_PATH="$BITNET_CPP_DIR/build/bin:$LD_LIBRARY_PATH"Cause: xtask built without the --features crossval-all (or --features inference).
Fix:
# Build xtask with required features
cargo run -p xtask --features crossval-all -- crossval-per-token --helpNote: The feature must be specified at build time, not at runtime.
Cause: Model path doesn't match detection heuristics.
Fix: Explicitly specify the backend:
# Force bitnet backend
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/my-model.gguf \
--tokenizer models/tokenizer.json \
--cpp-backend bitnet \
--prompt "Test"
# Force llama backend
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/my-model.gguf \
--tokenizer models/tokenizer.json \
--cpp-backend llama \
--prompt "Test"Cause: The wrong backend library was loaded, possibly due to LD_LIBRARY_PATH conflicts.
Fix:
# Check which libraries are in your library path
echo $LD_LIBRARY_PATH | tr ':' '\n'
# Clean up conflicting paths and use only bitnet.cpp
export LD_LIBRARY_PATH="$HOME/.cache/bitnet_cpp/build/bin:$LD_LIBRARY_PATH"
# Rebuild to clear any cached library info
cargo clean -p xtask -p bitnet-crossval
cargo build -p xtask --features crossval-allCause: Required C++ libraries weren't found during preflight checks (build time or runtime).
Fix:
# 1. Ensure BITNET_CPP_DIR is set at BUILD time
export BITNET_CPP_DIR="$HOME/.cache/bitnet_cpp"
export LD_LIBRARY_PATH="$BITNET_CPP_DIR/build/bin:$LD_LIBRARY_PATH"
# 2. Rebuild xtask to re-run build-time checks
cargo clean -p xtask -p bitnet-crossval
cargo build -p xtask --features crossval-all
# 3. Use --verbose to diagnose which libs are missing
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/model.gguf \
--tokenizer models/tokenizer.json \
--prompt "Test" \
--max-tokens 1 \
--verboseCause: Tokenizer mismatch between Rust and C++ implementations.
Fix:
# Use --dump-ids and --dump-cpp-ids to compare token sequences
cargo run -p xtask --features crossval-all -- crossval-per-token \
--model models/model.gguf \
--tokenizer models/tokenizer.json \
--prompt "What is 2+2?" \
--max-tokens 4 \
--dump-ids \
--dump-cpp-ids \
--verbose 2>&1 | head -50
# The output will show Rust vs C++ token IDs side-by-side
# If they diverge, investigate tokenizer differencesFor Windows, you'll need:
- Visual Studio 2019 or later (with C++ CMake tools)
- CMake 3.14+
- Add
-A x64to cmake if needed:cmake -B build -A x64 -DBUILD_SHARED_LIBS=ON cmake --build build --config Release -j
One-Click Helper: Llama-Build automates prerequisites and build on Windows.
BitNet-rs provides multiple levels of validation:
-
Smoke Test (
scripts/parity_smoke.sh):- One-command validation
- Tests both BitNet and QK256 formats
- Pretty-printed JSON receipts
-
Per-Token Parity (
xtask crossval-per-token):- Compares Rust vs C++ logits position-by-position
- Finds first divergence token
- Cosine similarity, L2 distance, max absolute difference
-
Trace Diffing (
scripts/trace_diff.py):- Layer-by-layer comparison
- Blake3 hash verification
- RMS statistics
- Identifies exact divergence point (layer, stage, sequence position)
-
Multi-Scenario Sweep (
scripts/run_crossval_sweep.sh):- 3 deterministic scenarios (1, 2, 4 tokens)
- 90+ trace files per scenario
- Summary markdown output
- C++ Reference: https://github.com/microsoft/BitNet
- llama.cpp Build Docs: https://github.com/ggml-org/llama.cpp
- BitNet-rs Cross-Validation:
docs/development/validation-framework.md - Trace Infrastructure:
crates/bitnet-trace/README.md - Per-Token Parity:
crossval/src/logits_compare.rs
docs/explanation/dual-backend-crossval.md- Dual-backend architecture and designdocs/development/validation-framework.md- Complete validation architecturedocs/howto/validate-models.md- Model validation workflowCLAUDE.md- Feature flags and cross-validation CLI reference