All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
(Nothing yet)
- Enhanced Streaming Inference: Async streaming with rich metadata for real-time applications
- LoRA Adapter Registry: Manage multiple adapters without reloading base model
- Production-Ready: Comprehensive tests, examples, and documentation
-
StreamTokentype: Rich metadata for each generated token- Token ID, decoded text (optional), probability, logit score, EOS flag
- Enables real-time confidence scores and debugging information
-
Async streaming API:
generate_stream_async()method- Returns
impl Stream<Item = Result<StreamToken>> - Full cancellation support via dropped streams
- Compatible with tokio and futures ecosystem
- Requires
streamingfeature flag
- Returns
-
Enhanced sync streaming: Updated
generate_stream()callback- Now passes
StreamTokeninstead ofu32(breaking change) - Provides probability and metadata for each token
- Backward compatible via
token.token_idaccess
- Now passes
-
AdapterRegistry: Centralized adapter management- Load, unload, activate, and deactivate adapters
- Multiple adapters in memory simultaneously
- Zero base model duplication
-
ApplyAdaptertrait: Full implementation for hot-swapping (#52)- Implemented
apply_adapter(),remove_adapter(),has_adapter()methods on Qwen model - Hot-swap adapters without model reload
- Integration tests and comprehensive documentation
- Implemented
-
Checkpoint integration: Load adapters from safetensors
load_adapter_from_checkpoint()methodadd_adapter()for pre-configured adapters- Efficient memory-mapped loading
-
streaming_demo.rs: Demonstrates streaming inference- Sync and async streaming
- Token metadata usage
- Early stopping and cancellation
-
adapter_swap_demo.rs: Demonstrates adapter management- Loading multiple adapters
- Switching between adapters
- Memory efficiency demonstration
-
Streaming tests: 14 comprehensive tests
- Async streaming with cancellation
- Metadata accuracy validation
- EOS token handling
- Probability distribution verification
-
Adapter hot-swap tests: 6 integration tests
- Registry workflow
- Checkpoint save/load
- Multiple adapter switching
- Error handling
- Memory efficiency validation
1. Streaming Callback Signature
Before (v1.2.x):
generator.generate_stream(&input_ids, |token: u32| -> bool {
println!("{}", token);
true
})?;After (v1.3.0):
generator.generate_stream(&input_ids, |token: StreamToken| -> bool {
println!("{} (prob: {:.2})", token.token_id, token.probability);
true
})?;Migration: Update callbacks to use token.token_id instead of token directly.
- N/A (new features, no bugs fixed)
Benchmarked on Apple M4 Max (16 cores, 48GB RAM), December 17, 2025:
-
✅ Adapter loading: Excellent performance across all ranks
- Rank 4: ~1.2ms
- Rank 8: ~2.5ms (typical use case)
- Rank 16: ~3.8ms
- Rank 32: ~5.4ms
- Rank 64: ~10.3ms
- All well under 500ms target
-
✅ Adapter switching: ~1.3ms
- Well under 100ms target
- No base model reload required
- Instant switching between adapters
-
✅ LoRA forward pass: No regression from v1.2.x
- Maintained baseline performance
- Efficient Metal GPU utilization
-
✅ Sync streaming overhead: 1.3-2.6% for typical workloads
- 10 tokens: +10.5% (fixed setup costs dominate short sequences)
- 50 tokens: +2.6% ← typical use case
- 100 tokens: +1.3% (overhead amortized across longer sequences)
- Target <5% met for sequences ≥50 tokens
-
✅ Async streaming overhead: 2.4-6.8% including tokio runtime
- 10 tokens: +10.5% (same fixed setup costs)
- 50 tokens: +6.8% ← typical use case
- 100 tokens: +2.4% (overhead decreases with length)
- Acceptable for concurrent/server applications
-
✅ Callback overhead: Negligible (<0.3%)
- String formatting: -0.08% (within noise)
- String accumulation: -0.24% (within noise)
- Design is highly efficient
-
✅ Sampling strategy overhead (vs Greedy):
- Top-k (50): +0.11% (essentially free)
- Top-p (0.9): +6.8% (sorting overhead, expected)
- Quality/performance trade-off is excellent
Notes:
- Adapter benchmarks show typical GPU variance (±10-20%) on shared hardware
- Streaming benchmarks use mock model on CPU for consistent, reproducible measurements
- All metrics validated with criterion (100 samples per benchmark)
- Complete API documentation for all new types
- Migration guide for breaking changes
- Two comprehensive examples
- Updated README with streaming and adapter examples
- All tests passing (195+ tests total)
- Code coverage: Maintained ≥80% threshold
- Zero clippy pedantic warnings
- Production-ready code quality
- Async streaming requires
streamingfeature:cargo build --features streaming - ApplyAdapter trait fully implemented for Qwen model in this release
- docs.rs Metadata Configuration: Fixed incorrect field name in
[package.metadata.docs.rs]- Changed
default-features = false→no-default-features = true - Added explicit
all-features = falsefor extra safety - This is why v1.2.2-v1.2.6 all failed!
- Changed
Looking at docs.rs build logs, discovered it was calling:
cargo rustdoc --lib --features "embeddings graph"
WITHOUT --no-default-features!
This meant default = ["custom-metal", "graph"] was STILL being enabled!
| Version | What We Fixed | Why It Failed |
|---|---|---|
| v1.2.2-v1.2.4 | Cargo.toml dependencies | Metadata used wrong field name |
| v1.2.5 | Removed src/backend/mps/ |
Metadata still wrong |
| v1.2.6 | Removed examples/mps_*.rs |
Metadata still wrong! |
| v1.2.7 | Fixed metadata field name | ✅ SHOULD WORK! |
- ✅ Cargo dependencies configured correctly (v1.2.4)
- ✅ Orphaned source modules removed (v1.2.5)
- ✅ Experimental examples removed (v1.2.6)
- ✅ docs.rs metadata field name corrected (v1.2.7) ← THIS WAS IT!
- ✅
no-default-features = true(correct field name) - ✅
all-features = false(belt AND suspenders) - ✅ Local docs build works
- ✅ No objc dependencies in tree
- docs.rs Build (Complete Fix): Moved MPS examples out of examples/ directory
examples/mps_lora_benchmark.rs→experiments/examples/mps_matmul_prototype.rs→experiments/examples/mps_matmul_simple.rs→experiments/- All three had unconditional
use objc::*statements - rustdoc scans examples even though they're dev-dependencies
- This was the ACTUAL root cause after v1.2.5 still failed
- v1.2.2-v1.2.4: Attempted various Cargo.toml configurations
- v1.2.5: Removed orphaned
src/backend/mps/module - v1.2.6: Removed experimental
examples/mps_*.rsexamples ← FINAL FIX
- ✅ No objc/metal deps without
custom-metalfeature - ✅ rustdoc builds successfully
- ✅ All tests passing
- ✅ Examples preserved in experiments/ directory
- The MPS examples were experimental prototypes never documented
- Moving them preserves code for future development
- No functional changes to library or public examples
- docs.rs Build: Remove orphaned MPS module causing Objective-C compilation
- Moved
src/backend/mps/toexperiments/mps/(orphaned experimental code) - MPS module was not exported in public API but rustdoc scanned it anyway
- Files had unconditional
use objc::*statements that triggered compilation - Referenced non-existent
mpsfeature (#![cfg(feature = "mps")]) - This was the root cause of v1.2.2-1.2.4 docs.rs failures
- Moved
- ✅ No Metal/Objective-C dependencies without
custom-metalfeature - ✅ Documentation builds successfully with
embeddings+graphfeatures - ✅ All 190 tests passing
- ✅ No functional changes to public API
- The MPS code was experimental/prototyping code never integrated into the library
- Moving it preserves the code for potential future use without affecting docs
- This should finally allow docs.rs to build successfully
- docs.rs Build: Disable default features on all Candle dependencies
candle-core,candle-nn,candle-transformersnow usedefault-features = false- This prevents transitive dependencies from pulling in Metal/Objective-C on Linux
- Completely eliminates
objc_exceptioncompilation error on docs.rs - All API documentation will now build successfully at https://docs.rs/metal-candle
The issue was that even though we made the metal feature optional on candle-core, the default features of candle-nn or candle-transformers were pulling in Metal-related dependencies transitively.
Solution: Disable default features on all Candle crates, then explicitly enable only what's needed via feature flags.
- No functional changes on macOS - Metal features work exactly as before
- All 190 tests passing
- Verified: No
objcormetaldependencies withoutcustom-metalfeature - v1.2.2 and v1.2.3 had incomplete fixes
- docs.rs Build: Properly configure dependencies to avoid Objective-C compilation on Linux
- Removed hardcoded
metalfeature fromcandle-corebase dependency - Made Metal features conditional via
custom-metalfeature flag - Feature syntax:
custom-metal = ["dep:metal", "dep:objc", "dep:candle-metal-kernels", "candle-core/metal"] - docs.rs builds with
embeddingsandgraphfeatures only (no Metal dependencies) - Fixes
objc_exceptioncompilation error on Linux - All API documentation now available at https://docs.rs/metal-candle
- Removed hardcoded
- No functional changes on macOS - Metal features work exactly as before
- Local builds and functionality unchanged
- v1.2.2 had incomplete fix that still pulled in Objective-C dependencies
- docs.rs Build: Initial attempt to configure docs.rs (incomplete - see v1.2.3)
- Added
[package.metadata.docs.rs]configuration - Made
candle-metal-kernelsoptional - Note: This version still failed on docs.rs due to hardcoded
candle-coremetal feature
- Added
- Superseded by v1.2.3 with complete fix
-
Benchmark Configuration: Added missing
[[bench]]entries forfused_lora_benchandlazy_vs_eagerbenchmarks inCargo.toml(#36)- Both benchmarks now properly execute and produce output
- No code changes required - purely a configuration fix
-
Embeddings Test: Confirmed
test_metal_layer_norm_metalnow passes consistently (#34)- Test failure was already resolved by Metal device initialization improvements in v1.2.0
- No additional fixes required
- All 407 tests passing (0 failures)
- Code coverage: 81.64% (maintained above 80% threshold)
- Zero clippy warnings (pedantic mode)
- Formatting verified
- This is a pure bugfix release with no breaking changes
- No functional code changes - only build configuration updates
- Fused Softmax Integration: Custom Metal kernel integrated into graph executor
- Benchmark Infrastructure: Automated CI smoke tests + official benchmark runner
- Improved Test Coverage: 216 tests (up from 173), comprehensive executor and loader coverage
- Release Process: Documented benchmark validation and release workflow
- Test Stability: Fixed 16 test failures from Candle Metal device initialization
- Fused Softmax Kernel: Integrated custom Metal kernel in graph executor (#27)
- 3.25x speedup for softmax operations on Metal devices (validated in PR #27)
- Automatic fallback to Candle implementation for CPU or non-last-dim operations
- Zero breaking changes - transparent performance improvement
- Benchmark validated on M4 Max (48GB RAM, macOS 26.1)
-
CI Smoke Tests: GitHub Actions workflow for automated regression detection
- Runs on every PR with low sample size (fast, ~2 minutes)
- Detects major performance bugs (>20% regression)
- Warning disclaimers about ±10-20% variance on shared hardware
-
Official Benchmark Runner:
scripts/run_official_benchmarks.sh- Environment validation (battery, CPU usage, thermal state)
- Multiple runs with cooldown periods (configurable: 5 runs, 60s cooldown)
- Results capture with environment snapshot
- Quick mode for script testing (
--quickflag) - Skip MLX comparisons (
--no-mlxflag)
-
Documentation: Comprehensive benchmark and release process docs
docs/BENCHMARK_CI.md: Benchmark strategy and methodology (320 lines)docs/RELEASE_PROCESS.md: Complete 5-phase release process (580 lines)docs/PR33_IMPLEMENTATION_SUMMARY.md: Implementation summary and rationale- Updated
CONTRIBUTING.md: Benchmark guidelines for contributors (+150 lines)
-
Executor Tests:
tests/executor_direct.rs(24 tests, 500 lines)- All executor operations tested (Matmul, Add, Mul, MulScalar, LoRA, Softmax, RMSNorm)
- Error handling validation for wrong input counts
- Broadcasting operations coverage
- Metal and CPU fallback path testing
-
Softmax Tests:
tests/softmax_lazy.rs(8 tests, 285 lines)- Lazy execution correctness validation
- Numerical stability testing (large values, edge cases)
- Fallback behavior testing (Metal vs CPU, different dimensions)
- Property validation (sum-to-one for softmax)
-
Embeddings Loader Tests:
tests/embeddings/loader_test.rs(11 tests, 136 lines)- Config loading (valid JSON, invalid JSON, missing files)
- Weights loading (safetensors, PyTorch, error cases)
- Test fixtures for reproducible validation
- Metal Device Initialization: Resolved Candle backend panic in tests (commit b27b2a1)
- Added panic guards with
AssertUnwindSafetoDevice::new_metal() - Implemented
OnceLockcaching foris_metal_available()to prevent race conditions - Suppressed panic output to avoid false test failures in CI
- Updated
custom_opsandmetal_opstests to usemetal_candle::Devicewrapper - Fixed 16 test failures, bringing passing tests from 173 to 189
- Related to Candle issue huggingface/candle#1355
- Added panic guards with
-
1 embeddings test failing (
test_metal_layer_norm_metal) - non-blocking- Unrelated to v1.2.0 changes
- Will be addressed in v1.2.1
-
Benchmark suite requires API updates for v1.1.0 compatibility
inferenceandtrainingbenchmarks use v1.0.0sample_tokenAPI- Need updates for v1.1.0 repetition penalty parameters
- Will be fixed in v1.2.1
- Does not affect fused softmax integration (already validated in PR #27)
None. All changes are backwards compatible.
- Complete benchmark CI strategy documentation
- Step-by-step release process with checklists
- Benchmark best practices for contributors
- Performance validation methodology
- Environment preparation guidelines
- Benchmark smoke tests run automatically on PRs but are NOT suitable for performance claims
- Official benchmarks must be run locally on controlled hardware for release validation
- See
docs/RELEASE_PROCESS.mdfor complete release workflow - Fused softmax performance claims validated in PR #27 on various hardware
- v1.2.0 focuses on integration, testing, and infrastructure improvements
- Production-Ready Text Generation API: Complete high-level API for text generation with streaming support
- Advanced Sampling Strategies: Repetition penalty for higher quality generation
- Comprehensive Testing: 203+ tests with full coverage of generation pipeline
- Developer Experience: New example demonstrating all generation features
The sample_token() function signature has been updated to support repetition penalty:
Before (v1.0.0):
pub fn sample_token(logits: &Tensor, strategy: &SamplingStrategy) -> Result<u32>After (v1.1.0):
pub fn sample_token(
logits: &Tensor,
strategy: &SamplingStrategy,
generated_ids: &[u32], // NEW: Previously generated tokens
repetition_penalty: f32, // NEW: Penalty factor (1.0 = no penalty)
) -> Result<u32>Migration Guide:
- For basic usage without repetition penalty: Pass
&[]and1.0as the new parameters - To enable repetition penalty: Pass your generated token history and desired penalty factor (e.g.,
1.1)
Example:
// Old code (v1.0.0)
let token = sample_token(&logits, &strategy)?;
// New code (v1.1.0) - no repetition penalty
let token = sample_token(&logits, &strategy, &[], 1.0)?;
// New code (v1.1.0) - with repetition penalty
let token = sample_token(&logits, &strategy, &generated_ids, 1.1)?;Recommended: Use the high-level Generator API instead of calling sample_token() directly:
let mut generator = Generator::new(Box::new(model), config)?;
let output = generator.generate(&input_ids)?;Generatorstruct: High-level text generation with model integrationgenerate(): Standard generation with configurable parametersgenerate_stream(): Real-time streaming generation with callback support- Stop conditions: EOS tokens, custom stop tokens, max length
- Automatic repetition penalty application
LanguageModeltrait: Common interface for different model architectures- Implemented for
Qwenmodel - Extensible for future model architectures
- Implemented for
- Generation Examples: New
examples/generate_text.rsdemonstrating:- Basic greedy generation
- Different sampling strategies (Greedy, Top-k, Top-p, Temperature)
- Streaming generation with callbacks
- Repetition penalty usage
- Stop conditions
- Repetition Penalty:
apply_repetition_penalty()function- Reduces repetitive text generation
- Configurable penalty factor (> 1.0 = penalize, 1.0 = no penalty)
- Integrated with all sampling strategies
- Enhanced
sample_token(): Now accepts repetition penalty and generated token history
- Complete
GeneratorConfig:- All sampling parameters:
temperature,top_p,top_k,repetition_penalty - Stop conditions:
stop_on_eos,eos_token_id,stop_tokens - Builder-friendly with sensible defaults
- All sampling parameters:
- Extended test coverage: 210+ tests (up from 195)
- Unit tests for
Generatorwith mock models - Integration tests for full generation pipeline
- Tests for all sampling strategies and stop conditions
- Tests for streaming API and callbacks
- Unit tests for
- Code coverage: Maintained ≥80% coverage
- Zero clippy warnings: Pedantic mode with production-quality code
GeneratorAPI: Replaced placeholder with full implementation (see Breaking Changes section forsample_token()updates)
- N/A (new features, no bugs fixed)
- Generation performance: Comparable to v1.0.0 inference (no KV-cache optimization yet)
- Sampling overhead: <1% of forward pass time (maintained)
- Memory: Minimal overhead for repetition penalty tracking
- Complete API documentation for all new types and functions
- New example (
generate_text.rs) with 5 comprehensive demos - Updated README with text generation quick start
- Inline code examples in docstrings
- #29: Advanced Sampling Strategies for Text Generation ✅
- #30: KV Cache Implementation (Already complete in v1.0.0) ✅
- #31: High-Level Text Generation API ✅
-
Issue #27 (Custom Fused Softmax Kernel): Deferred to v1.2.0 per release plan
- Reason: Text generation API provides more immediate user value
- Current Candle softmax performs adequately
- Will optimize in v1.2.0 after full pipeline validation
-
Generator KV-Cache Optimization: Planned for v1.2.0
- Current implementation passes all tokens on each forward pass
- Future optimization will use incremental approach (only pass last token)
- This will significantly improve generation performance for longer sequences
- Does not affect API compatibility
1.0.0 - 2024-12-10
- 25.9x faster than MLX for embeddings (Apple's official ML framework)
- Production-ready LoRA training for Apple Silicon
- Custom Metal LayerNorm kernel for optimal performance
- Lazy evaluation graph with operation fusion (experimental, feature-gated)
- 190 passing tests (137 lib + 53 doc), 84.69% code coverage
- Clean codebase: 4 documented pedantic warnings, 100% API documentation
-
LoRA Training Pipeline: Complete Low-Rank Adaptation implementation for efficient fine-tuning
- LoRA layers with configurable rank and alpha parameters
- Support for Q-Proj, K-Proj, V-Proj, and O-Proj target modules
- Dropout support: Training/eval mode control for regularization (per LoRA paper)
- Gradient flow verification and backpropagation support
- Performance: Metal GPU delivers 1.76-3.14x speedup over CPU for LoRA operations
-
Model Loading & Architecture:
- Safetensors format support with validation
- Qwen2.5-Coder architecture implementation
- Transformer components: RoPE embeddings, multi-head attention (GQA), MLP layers
- Model configuration from JSON files
- Builder pattern API with sensible defaults
-
Training Infrastructure:
- AdamW optimizer with decoupled weight decay
- Learning rate schedulers: Constant, Linear, Cosine, WarmupCosine
- Cross-entropy loss with optional label smoothing
- Gradient clipping and accumulation
- Checkpoint management (save/load with metadata)
-
Inference & Text Generation:
- KV-cache for efficient token generation (~173 MB for 2048 tokens, Qwen 0.5B F16)
- Multiple sampling strategies: Greedy, Top-k, Top-p (nucleus), Temperature
- Memory-efficient O(1) position tracking
- Sampling overhead <1% of forward pass time
-
Semantic Embeddings (feature:
embeddings):- Sentence-transformers support: E5-small-v2, MiniLM-L6-v2, MPNet-base-v2
- HuggingFace Hub integration with auto-download and caching
- Mean pooling with attention weighting
- L2 normalization for cosine similarity
- Custom Metal LayerNorm kernel: 25.9x faster than MLX for batch processing
- Works on both CPU and Metal devices
-
Metal Acceleration:
- Native Apple Silicon Metal backend via Candle
- Custom Metal LayerNorm kernel for optimal embeddings performance
- Near constant-time performance (4.4ms for 100 docs, 3.9ms for 1 doc)
- Lazy evaluation graph with operation fusion
- 195 comprehensive tests: 187 library tests (including 8 dropout tests) + 56 doctests
- Clean codebase: 4 documented pedantic warnings (all justified and documented)
- Code coverage: Exceeds 80% requirement
- 100% API documentation: All public APIs fully documented with examples
- 6 working examples: Demonstrating all major features
- Complete architecture documentation: ARCHITECTURE.md, CONTRIBUTING.md, performance guides
- Embeddings: 25.9x faster than MLX for batch processing (100 docs: 4.4ms vs 113.5ms)
- Single Query: 2x faster than MLX (3.9ms vs 7.7ms)
- Throughput: 22,831 docs/sec (MLX: 881 docs/sec)
- Near Constant-Time: Only 13% increase for 100x more data (3.9ms → 4.4ms)
- KV-Cache: Minimal overhead, <1% of generation time
- N/A (initial release)
- N/A (initial release)
- N/A (initial release)
- N/A (initial release)
- No known security vulnerabilities
- All dependencies audited with
cargo deny - Two unmaintained transitive dependencies (not security issues):
number_prefix(via hf-hub → indicatif)paste(via candle-core → gemm/metal)- Both from trusted upstream, will be resolved when dependencies update
Detailed benchmarks available in MLX_BENCHMARK_COMPARISON.md and PERFORMANCE_SUMMARY.md.
| Batch Size | metal-candle | MLX | Speedup |
|---|---|---|---|
| 1 | 3.9ms | 7.7ms | 2.0x |
| 100 | 4.4ms | 113.5ms | 25.9x |
Throughput: 22,831 docs/sec (MLX: 881 docs/sec)
- Custom LayerNorm kernel with optimal threadgroup sizing
- Lazy evaluation graph with operation fusion
- Near constant-time scaling across batch sizes
- Model Format: Safetensors only (GGUF planned for v1.1+)
- Model Architecture: Qwen2.5-Coder for text generation, BERT variants for embeddings
- Apple Silicon Only: Requires M1/M2/M3/M4 chip with Metal support
- Single GPU: Multi-GPU support planned for v2.0
- Best for: Semantic embeddings and RAG applications (25.9x faster than MLX)
- Great for: LoRA training and fine-tuning
- Excellent for: Inference with LoRA adapters
- Production Ready: Use Metal for all embeddings workloads
This is the initial v1.0.0 release. No upgrade path needed.
For users migrating from Ferris project's MLX+PyO3 implementation:
- Remove Python dependencies: No Python runtime or virtual environment needed
- Update model loading: Use
ModelLoaderbuilder API - Update LoRA training: Use
LoRAAdapterandTrainerAPIs - Performance: Expect 1.5-2.4x speedup for LoRA operations
- Deployment: Single binary, no Python packaging needed
See migration guide in documentation for detailed steps.
- GGUF format support
- Additional model architectures (LLaMA, Mistral)
- Optional transformer component optimization
- Advanced LoRA variants (DoRA, etc.)
- Quantization support (4-bit, 8-bit)
- Flash Attention integration
- Streaming generation with callbacks
- Batched inference optimization
- Multi-GPU training support
- Custom Metal kernel implementations
- Model quantization and compression
- @GarthDB - Initial implementation and design
- Built on the excellent Candle framework by Hugging Face
- Inspired by MLX and llama.cpp
- LoRA implementation based on LoRA paper by Hu et al.
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Status: ✅ Production Ready
Target Platform: Apple Silicon (M1/M2/M3/M4)
Minimum Requirements: Rust 1.75+, macOS 12.0+