Welcome to BitNet-rs! We appreciate your interest in contributing to our high-performance 1-bit neural network quantization and inference library for Rust.
-
Fork and Clone
git clone https://github.com/your-username/BitNet-rs.git cd BitNet-rs -
Setup Development Environment
# Install Rust (MSRV defined in rust-toolchain.toml) rustup update stable # Install development tools cargo install cargo-nextest cargo-mutants # Install ripgrep (required for pre-commit hooks) # macOS: brew install ripgrep # Ubuntu: sudo apt-get install ripgrep # Windows: choco install ripgrep # Or visit: https://github.com/BurntSushi/ripgrep # Enable pre-commit hooks (run once after cloning) git config core.hooksPath .githooks
-
Run Tests
# Quick test with CPU features cargo test --locked --workspace --no-default-features --features cpu # CI-equivalent local smoke test ./ci/local.sh
BitNet-rs uses local pre-commit hooks to catch quality issues before they reach CI.
The hooks are enabled automatically if you followed the setup instructions above. If not:
git config core.hooksPath .githooks- ripgrep (
rgcommand) is required for pattern matching- Install via your package manager (see setup instructions)
- Hooks will fail gracefully with instructions if
rgis not found
The pre-commit hook enforces two critical quality gates:
All ignored tests must include a justification. Valid patterns:
// ✅ Attribute style
#[ignore = "Blocked by Issue #254 - shape mismatch"]
fn test_something() { ... }
// ✅ Inline comment
#[ignore] // Slow: QK256 scalar kernels (~0.1 tok/s)
fn test_slow_operation() { ... }
// ✅ Preceding comment (within 2 lines)
// Blocked by Issue #254 - shape mismatch in layer-norm
#[ignore]
fn test_layer_norm() { ... }
// ❌ Bare ignore (rejected by hook)
#[ignore]
fn test_something() { ... }Valid justification patterns:
Blocked by Issue #NNNIssue #NNN(shorthand)Slow: <reason>TODO: <reason>FIXME: <reason>
Tests that modify environment variables must use the EnvGuard pattern:
// ❌ Raw mutation (rejected by hook)
#[test]
fn test_deterministic() {
unsafe {
std::env::set_var("BITNET_DETERMINISTIC", "1");
}
// ...
}
// ✅ EnvGuard pattern (accepted)
use serial_test::serial;
use tests::support::env_guard::EnvGuard;
#[test]
#[serial(bitnet_env)] // Ensures serial execution
fn test_deterministic() {
let _guard = EnvGuard::new("BITNET_DETERMINISTIC");
_guard.set("1");
// Test code - env automatically restored on drop
}Why: Raw std::env::set_var is unsafe and can cause race conditions in parallel test execution. The EnvGuard pattern ensures:
- Thread-safe mutations via global mutex
- Process-safe execution via
#[serial(bitnet_env)] - Automatic restoration of original values on scope exit
Hook is slow:
- Ensure ripgrep is installed (hooks check patterns in ~15k files)
- Check disk I/O if repo is on network storage
False positives:
- Review the hook output - it shows which file/line failed
- Ensure your code follows the patterns above
- Check
docs/development/test-suite.mdfor more examples
Bypass hooks temporarily (emergency only):
git commit --no-verifyThe pre-commit hooks use the same validation scripts as CI:
.githooks/pre-commit→scripts/lib/ignore_check.sh- CI guards →
scripts/lib/ignore_check.sh
This ensures local validation exactly matches CI, preventing surprise failures.
-
Create Feature Branch
git checkout -b feature/your-feature-name
-
Follow TDD Approach
- Write tests first
- Implement minimal code to pass tests
- Refactor with safety
See ROADMAP.md for the project's current direction and priorities.
- Use xtask Commands
# Download test models cargo run --locked -p xtask -- download-model # Verify implementation cargo run --locked -p xtask -- verify --model models/test.gguf # Cross-validate against C++ reference cargo run --locked -p xtask -- crossval
New internal developer workflows belong in xtask. bitnet-task exists only as a compatibility facade for migrated scripts/*.sh entrypoints, so only add to it when preserving an existing shell contract during migration.
- MSRV: See
rust-toolchain.tomlfor the pinned toolchain version (Rust 2024 edition) - Features: Always specify
--no-default-features --features cpu|gpu - Safety: Minimize
unsafecode; document all usage - Performance: Target >99% quantization accuracy
- Testing: Maintain 100% test coverage for critical paths
- Quantization: Support I2S, TL1, TL2, and IQ2_S formats
- GPU Support: CUDA kernels with CPU fallback
- GGUF Compatibility: Maintain compatibility with upstream formats
- Cross-validation: All changes must pass C++ reference comparison
- API Documentation: All public APIs must have comprehensive rustdoc
- Examples: Include working examples for new features
- Performance: Document performance characteristics and benchmarks
- Migration: Update migration guides for breaking changes
-
Unit Tests
cargo test --locked --workspace --no-default-features --features cpu -
Integration Tests (CPU golden path)
cargo test --locked -p bitnet-inference --test cpu_golden_path --no-default-features --features cpu -
Cross-validation Tests
export BITNET_GGUF="models/test.gguf" cargo test --locked --package crossval --no-default-features --features cpu
-
Property Tests
cargo test --locked property_ --no-default-features --features cpu -
Mutation Tests (CI only)
cargo mutants --package bitnet-quantization
BitNet-rs uses a 3-layer fixture architecture for testing neural network operations, quantization algorithms, and model loading:
-
Inline Fixtures — Hardcoded test data in the test file
- Use for: Trivial constants, magic numbers, simple shapes
- Example:
let vocab_size = 32000;
-
Generated Fixtures — Programmatic test data from seed functions
- Use for: Randomized tensors, quantization roundtrip tests, property-based testing
- Example:
helpers::lcg_random(seed)for deterministic random data - Available generators:
lcg_random(seed)— Deterministic LCG PRNG for numeric datagenerate_gguf_fixture(config)— Minimal GGUF files for tokenizer/model testscreate_test_gguf_with_i2s(name, shape, data, type)— I2S quantized tensor fixtures
-
File-Based Fixtures — External test data loaded from disk
- Use for: Full GGUF models, real tokenizer files, reference data
- Example:
models/test.gguf(gated byBITNET_GGUFenv var; not committed to repo)
Deterministic Random Data (LCG PRNG):
use helpers::lcg_random;
#[test]
fn test_quantization_roundtrip() {
let seed = 42;
let input: Vec<f32> = (0..1024).map(|_| lcg_random(seed) as f32).collect();
// Quantize → Dequantize → Validate
let quantized = quantize_i2s(&input);
let dequantized = dequantize_i2s(&quantized);
assert!(calculate_correlation(&input, &dequantized) > 0.99);
}GGUF Model Fixtures:
use fixtures::gguf_fixtures::{generate_gguf_fixture, GgufFixtureConfig};
#[test]
fn test_tokenizer_auto_discovery() {
let temp_dir = TempDir::new().unwrap();
let model_path = temp_dir.path().join("test.gguf");
generate_gguf_fixture(&model_path, &GgufFixtureConfig {
model_type: "llama".to_string(),
vocab_size: 128256,
has_embedded_tokenizer: true,
tokenizer_type: Some("hf".to_string()),
corrupted: false,
}).unwrap();
// Test tokenizer discovery logic
let tokenizer = load_tokenizer(&model_path).unwrap();
assert_eq!(tokenizer.vocab_size(), 128256);
}I2S Quantized Tensor Fixtures:
#[test]
fn test_qk256_flavor_detection() {
// Generate QK256 quantized tensor (256-elem blocks, separate scales)
let rows = 4;
let cols = 256;
let data = vec![0u8; rows * cols / 4 + rows * 2]; // 2-bit packed + scales
let file = create_test_gguf_with_i2s("test.weight", &[rows, cols], data, 26);
// Validate flavor detection
let result = load_gguf_full(file.path(), Device::Cpu).unwrap();
assert!(result.i2s_qk256.contains_key("test.weight"));
}# Run all tests (includes fixture-based tests)
cargo test --locked --workspace --no-default-features --features cpu
# Run specific fixture tests
cargo test --locked -p bitnet-models test_qk256_flavor_detection --no-default-features --features cpu
cargo test --locked -p bitnet-tokenizers test_gguf_fixture --no-default-features --features cpu
# Skip slow fixture-heavy tests (e.g., full model loading)
BITNET_SKIP_SLOW_TESTS=1 cargo test --locked --workspace --no-default-features --features cpuStep-by-step guide:
-
Identify fixture requirements — What test data do you need?
- Simple constants? → Use inline fixtures
- Randomized tensors? → Use generated fixtures with seeded PRNG
- Full models? → Use file-based fixtures or
generate_gguf_fixture
-
Choose appropriate layer
// Inline: trivial constants let embedding_dim = 512; // Generated: seeded random data let weights = helpers::generate_random_tensor(42, shape); // File-based: real model artifacts let model = load_gguf("tests/fixtures/gguf/llama3-128k.gguf");
-
Add generator function if needed (for new fixture types)
// In crates/bitnet-models/tests/helpers/qk256_fixtures.rs pub fn generate_qk256_4x256(seed: u64) -> Vec<u8> { // Generate deterministic QK256 quantized tensor let mut rng = lcg_random(seed); let rows = 4; let cols = 256; let data_size = rows * cols / 4 + rows * 2; // 2-bit packed + scales (0..data_size).map(|_| (rng() % 256) as u8).collect() }
-
Write test using fixture
#[test] fn test_new_quantization_format() { let data = helpers::generate_qk256_4x256(42); let quantized = QuantizedTensor::from_bytes(&data); assert_eq!(quantized.shape(), &[4, 256]); }
-
Document fixture purpose — Add rustdoc comments
/// Generate QK256 quantized tensor with deterministic seed /// /// # Format /// - 4 rows × 256 columns (1024 elements) /// - 2-bit packed representation (256 bytes) /// - Separate FP16 scales (8 bytes) /// /// # Seed /// Use consistent seeds for reproducibility: /// - `42` — Default test seed /// - `1337` — Edge case seed (extreme values) pub fn generate_qk256_4x256(seed: u64) -> Vec<u8> { /* ... */ }
Best practices:
- Use seeded generators for reproducible randomness (never
rand::thread_rng()) - Keep inline fixtures minimal and well-commented
- Prefer generated fixtures over hardcoded binary blobs
- Use file-based fixtures sparingly (increases repo size)
- Document fixture format and seed meanings in rustdoc
See also:
tests/helpers/issue_261_test_helpers.rs— Quantization accuracy helperscrates/bitnet-tokenizers/tests/fixtures/gguf_fixtures.rs— GGUF fixture generatorcrates/bitnet-models/tests/qk256_dual_flavor_tests.rs— I2S fixture examples
# Requires CUDA toolkit
cargo test --locked --workspace --no-default-features --features gpuBitNet-rs uses environment variables for runtime configuration (e.g., BITNET_STRICT_MODE, BITNET_DETERMINISTIC, BITNET_GGUF). Tests that mutate environment variables must use EnvGuard and serial execution to prevent flaky tests and race conditions.
Why This Matters: Environment variables are process-global state. Without isolation, parallel tests can interfere with each other:
- Test A sets
BITNET_STRICT_MODE=1→ Test B unexpectedly sees strict mode enabled - Test C clears
BITNET_GGUF→ Test D's model path is lost - Non-deterministic failures occur depending on test execution order
Using EnvGuard for Safe Environment Mutations:
use tests::support::env_guard::EnvGuard;
use serial_test::serial;
#[test]
#[serial(bitnet_env)] // Prevents parallel execution with other env-mutating tests
fn test_strict_mode_validation() {
// EnvGuard automatically restores original env state on drop
let guard = EnvGuard::new("BITNET_STRICT_MODE");
guard.set("1");
// Test code that depends on BITNET_STRICT_MODE=1
let result = validate_model("models/test.gguf");
assert!(result.is_err(), "Expected strict mode to fail on warnings");
// guard drops here → BITNET_STRICT_MODE restored to original value
}
#[test]
#[serial(bitnet_env)] // Same serial group ensures sequential execution
fn test_model_path_override() {
let guard = EnvGuard::new("BITNET_GGUF");
guard.set("/tmp/test.gguf");
// Test code that uses BITNET_GGUF env var
assert_eq!(get_model_path(), Some("/tmp/test.gguf".into()));
}Best Practices:
- Always use
#[serial(bitnet_env)]on tests that callEnvGuard::new()or directly mutate environment variables - Group related env tests under the same serial tag (e.g.,
bitnet_env) to prevent cross-contamination - Document env dependencies in test rustdoc comments (e.g.,
/// Requires BITNET_STRICT_MODE to be unset) - Avoid direct
std::env::set_var— useEnvGuardinstead for automatic cleanup - Run env tests sequentially in CI to ensure reproducibility (nextest already handles this via
#[serial])
See also:
tests/support/env_guard.rs— EnvGuard implementationcrates/bitnet-common/tests/issue_260_strict_mode_tests.rs— EnvGuard usage examplescrates/bitnet-models/tests/loader_strict_mode.rs— Serial env testing patterns
BitNet-rs provides Git hooks that enforce quality standards locally before commits reach CI:
# Enable pre-commit hooks
git config core.hooksPath .githooksWhat the hooks check:
- ✅ #[ignore] Annotation Hygiene: Ensures all
#[ignore]attributes include a reason ⚠️ Environment Mutation Safety: Warns about rawstd::env::set_var()calls (should use EnvGuard)
See: .githooks/README.md for full documentation
Pre-commit hooks catch issues early in your local workflow:
- Faster feedback than waiting for CI (seconds vs minutes)
- Prevents accidental commits of bare
#[ignore]markers - Enforces EnvGuard pattern for environment mutations
- Saves CI resources by catching issues before push
Note: You can temporarily bypass hooks with git commit --no-verify, but CI will still enforce these checks.
BitNet-rs enforces strict CI hygiene and supply chain security to prevent supply chain attacks and ensure reproducible builds:
GitHub Actions Supply Chain:
- All workflow actions must be SHA-pinned (not floating tags like
@v3) - Automated weekly repin workflow (
repin-actions.yml) updates pins while maintaining security - CI blocks PRs that introduce floating action references via the Guards gate
Dependency Determinism:
- All
cargo/crossinvocations use--locked(77+ invocations across workflows) - Ensures reproducible builds by enforcing exact dependency versions from
Cargo.lock - CI blocks PRs that add non-locked cargo/cross commands via the Guards gate
MSRV Enforcement:
- Minimum Supported Rust Version (MSRV): Defined in
rust-toolchain.toml(Rust 2024 edition) - All workflows must use
rust-toolchain.tomlfor MSRV consistency - CI blocks PRs that hardcode toolchain versions outside
rust-toolchain.toml
Runner Pinning:
- Default runners are pinned to
ubuntu-22.04for single-platform deterministic CI lanes - Intentional
ubuntu-latestusage is allowed for:- Security scanning workflows (e.g.,
security.yml) - should track latest tools - Coverage tracking workflows (e.g.,
coverage.yml) - benefits from latest tooling - Compatibility smoke testing (e.g.,
compatibility.yml) - explicitly tests across versions
- Security scanning workflows (e.g.,
- When adding new workflows: Default to
ubuntu-22.04for reproducibility. Ifubuntu-latestis required, justify in PR and add an inline comment explaining why.
Receipt Workflow Hygiene:
- Receipt verification builds must exclude Python/WASM bindings (
--exclude bitnet-py --exclude bitnet-wasm) - Required in both CPU and GPU build lanes to prevent libpython and WASM linker dependencies
- CI blocks PRs that remove these excludes from
verify-receipts.ymlvia the Guards gate - Keeps receipt verification hermetic and reproducible across CI environments
Violations will fail the required "Guards" check and block merge. See .github/workflows/guards.yml for detailed enforcement rules.
Recommended: One-command preflight check
# Run all guards checks with clear output (fail-fast)
make guards # or make preflightThis single command checks:
- ✅ Floating action refs (no @v1, @main, @stable, @latest)
- ✅ 40-hex SHA pins (external actions must use full commit SHA)
- ✅ MSRV consistency (must match rust-toolchain.toml)
- ✅ cargo/cross --locked flags (all build/test/run/bench/clippy)
- ✅ Receipt workflow excludes (bitnet-py/bitnet-wasm in CPU+GPU lanes)
Other helpers:
-
Add
--lockedto workflow commands safely (handlescargo run … -- …):scripts/fix-locked.sh .github/workflows/*.yml -
Validate CODEOWNERS team slugs (requires
ghauth):scripts/check-codeowners-teams.sh
-
Run guards locally (individual checks, for debugging):
# Check for floating action refs (no @v1, @main, @stable, @latest) rg --glob '!guards.yml' 'uses:.*@v[0-9]|uses:.*@(main|stable|latest)' .github/workflows || echo "OK: pinned" # Check for 40-hex SHA pins (external actions must use full commit SHA) rg --glob '!guards.yml' '^\s*uses:\s*(?!\./)[^ @]+/[^ @]+@(?![0-9a-f]{40}\b)' .github/workflows || echo "OK: 40-hex" # Check MSRV consistency (must match rust-toolchain.toml) rg --glob '!guards.yml' 'toolchain:\s*"?1\.90\.0"?|rust-version\s*=\s*"1\.90\.0"|\"RUST_VERSION\"\s*:\s*\"1\.90\.0\"' .github/workflows || echo "OK: MSRV" # Check cargo/cross --locked everywhere rg --glob '*.yml' --glob '!guards.yml' 'cargo (build|test|run|bench|clippy)' .github/workflows | grep -v -- '--locked' || echo "OK: locked"
Before submitting a PR, ensure:
- Run local guards check -
make guardspasses locally before push - Actions are SHA-pinned - No floating tags (@v3, @main, @stable, @latest); all external actions must use 40-hex commit SHAs
- Cargo/cross commands use
--locked- Allcargo/cross build/test/run/bench/clippyinclude--locked - MSRV compliance - Toolchain version must match
rust-toolchain.toml, no hardcoded versions in workflows - Guards check is green - CI will automatically validate these requirements (but catch issues early with
make guards)
-
Enable Pre-commit Hooks (first time only)
git config core.hooksPath .githooks
-
Run Local Preflight Guards (Recommended - CI alignment check)
# Quick preflight: Check CI guards (floating refs, MSRV, --locked flags) make guards # or make preflight
This verifies:
- ✅ All GitHub Actions are SHA-pinned (no floating @v3, @main, etc.)
- ✅ All action pins use 40-hex commit SHAs (immutable)
- ✅ MSRV consistency (1.92.0 — see
rust-toolchain.toml) - ✅ All cargo/cross commands use
--lockedflags
Why run this locally? Catches CI blockers before push, saving CI minutes and iteration time.
-
Run Local Quality Gates (Recommended - full validation)
# CI-equivalent local smoke: fmt → clippy → build → test (core pkgs) ./ci/local.shOr run individual checks:
-
Format and Lint
cargo fmt --all cargo clippy --locked --all-targets --all-features -- -D warnings
-
Run Full Test Suite
cargo nextest run --locked --workspace --no-default-features --features cpu # Or with cargo test cargo test --locked --workspace --no-default-features --features cpu
-
Verify Inference Receipt (if you have ci/inference.json)
# Verify CPU receipt cargo run --locked -p xtask -- verify-receipt --path ci/inference.json # Verify GPU receipt (requires GPU kernels) cargo run --locked -p xtask -- verify-receipt --path ci/inference.json --require-gpu-kernels
-
Update Documentation
cargo doc --locked --workspace --no-default-features --features cpu --no-deps
-
Cross-validate Changes (optional, for inference changes)
cargo run --locked -p xtask -- full-crossval
- Title: Use conventional commits format (
feat:,fix:,docs:) - Description: Include what, why, and testing performed
- Tests: All new code must include tests
- Documentation: Update relevant documentation
- Backwards Compatibility: Document any breaking changes
- Automated Checks: All CI checks must pass
- Code Review: At least one maintainer approval required
- Cross-validation: Must pass accuracy validation
- Performance: No significant performance regressions
BitNet-rs uses a label-gated CI system to optimize CI resource usage. By default, only fast core checks run on every PR:
- Build & Test (ubuntu-latest, ~5 minutes)
- Clippy (linting)
- Documentation (doc generation)
- CI Core Success (aggregate gate)
Optional label-triggered workflows run heavier validation on-demand. Add these labels to your PR to trigger additional checks:
| Label | Workflow | Description | Typical Runtime |
|---|---|---|---|
gpu |
GPU Tests | CUDA kernel validation (requires GPU runner) | ~10-15 min |
quant |
Quantization Matrix CI | Build & test quantization features (I2S, TL1, TL2, IQ2_S) | ~15-20 min |
crossval |
Cross-Validation Determinism | Compare Rust vs C++ reference implementation | ~20-30 min |
perf |
Performance Regression Gate | Benchmark validation against baselines | ~20-30 min |
integration |
Integration Tests | Full integration test suite | ~15-25 min |
receipts |
Receipt Verification | Verify inference receipt quality gates | ~10-15 min |
coverage |
Code Coverage | Generate coverage report (70% threshold) | ~10-15 min |
stress |
TL LUT Stress Tests | Deterministic stress tests for table lookup kernels | ~30-45 min |
When to use labels:
gpu: Required for GPU kernel changes, CUDA optimizations, or device selection logicquant: Required for quantization algorithm changes (I2S, TL1, TL2, IQ2_S)crossval: Recommended for inference engine changes, ensures parity with C++ referenceperf: Required for performance-critical changes, prevents regressionsintegration: Recommended for multi-crate changes, end-to-end validationreceipts: Required for receipt schema changes, kernel ID validationcoverage: Optional, useful for tracking test coverage improvementsstress: Optional, validates deterministic behavior under heavy load
Example workflow:
# 1. Create PR (core checks run automatically)
gh pr create --title "feat: optimize QK256 dequantization" --body "..."
# 2. Add labels to trigger optional checks
gh pr edit 123 --add-label quant,perf,crossval
# 3. CI runs: core + quantization + performance + cross-validation
# 4. Remove labels after validation (optional, to save CI minutes on subsequent pushes)
gh pr edit 123 --remove-label quant,perf,crossvalCI Configuration Notes:
- Timeouts: All jobs have appropriate timeouts (15-60 minutes) to prevent hangs
- Continue-on-error: Some jobs (like
coverage,crossval) are informational during stabilization - Branch protection: Only the 4 core checks are required; optional jobs are skipped by default
- Workflow dispatch: Most label-gated workflows can also be triggered manually via GitHub Actions UI
See also: .github/workflows/ for individual workflow definitions and trigger conditions.
If you're contributing GPU backend code (kernels, new backends, device selection), see the dedicated GPU guides:
- GPU Contributor Guide — Step-by-step guides for adding kernels and backends, testing without hardware, and GPU code style conventions
- GPU Development Workflow — Branch naming, PR templates, CI pipeline, review checklists, and performance regression detection
Quick start for GPU contributors:
# Build and test without GPU hardware
cargo build --locked --no-default-features --features cpu
cargo nextest run --locked --workspace --no-default-features --features cpu
# Build with GPU support (requires CUDA toolkit)
cargo build --locked --no-default-features --features gpu
# Test GPU code paths without hardware
BITNET_GPU_FAKE=1 cargo test --locked -p bitnet-kernels --no-default-features --features gpuGPU branches use the intel-gpu/<feature-name> naming convention. Add the gpu label
to your PR to trigger GPU-specific CI checks on self-hosted runners.
bitnet: Main library with unified public APIbitnet-quantization: Quantization algorithms (I2S, TL1, TL2)bitnet-kernels: High-performance SIMD/CUDA kernelsbitnet-inference: Inference engine with streamingbitnet-models: Model loading (GGUF, SafeTensors)bitnet-tokenizers: Universal tokenizer with GGUF integration
- Zero-Copy: Minimize allocations and copies
- Device-Aware: Automatic GPU/CPU selection
- Type Safety: Leverage Rust's type system for correctness
- Performance: Target high-performance computing workloads
- Compatibility: Maintain API stability and GGUF compatibility
- Documentation: docs/ directory with comprehensive guides
- Examples: examples/ directory with working code
- Issues: GitHub Issues for bug reports and feature requests
- Discussions: GitHub Discussions for questions and ideas
By contributing to BitNet-rs, you agree that your contributions will be licensed under the same terms as the project (MIT OR Apache-2.0).
Please read and follow our Code of Conduct.
Thank you for contributing to BitNet-rs! Your contributions help advance high-performance neural network inference in Rust.