-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy path.cursorrules
More file actions
618 lines (490 loc) · 14.7 KB
/
Copy path.cursorrules
File metadata and controls
618 lines (490 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
# metal-candle Cursor Rules
## Project Overview
`metal-candle` is a production-quality Rust crate for machine learning on Apple Silicon, built on Candle with Metal backend. Focus: LoRA training, model loading (safetensors), and text generation for transformer models.
**Target Quality**: Publication-ready crate for crates.io
**Timeline**: 12 weeks to v1.0
**Key Goals**: Replace PyO3+MLX with pure Rust, single-binary deployment
## Core Principles
1. **Quality Over Speed**: Every commit meets production standards
2. **Explicit Over Implicit**: Clear, readable code preferred over clever tricks
3. **Fail Fast**: Use `Result` and `?` operator, not unwrap/expect
4. **Document Everything**: Public APIs fully documented with examples
5. **Test Everything**: Unit tests for all core functionality, integration tests for workflows
## Code Quality Standards
### Clippy - ZERO WARNINGS ENFORCED
```toml
[lints.clippy]
pedantic = "deny" # Strict pedantic linting (not nursery)
cargo = "warn"
all = "deny"
correctness = "deny"
suspicious = "deny"
complexity = "deny"
perf = "deny"
```
**Local Development**:
```bash
cargo clippy -- -D warnings # Must pass before commit
```
**Allowed Exceptions** (add to Cargo.toml with justification):
- `module_name_repetitions` - Only if truly unavoidable
- `missing_errors_doc` - Never allow, always document errors
- `missing_panics_doc` - Never allow, always document panics
### Code Coverage - ≥80% ENFORCED
**Measure Coverage**:
```bash
cargo llvm-cov --all-features --workspace --html
open target/llvm-cov/html/index.html
```
**Target Coverage by Module**:
- Public APIs: 100%
- Core algorithms (LoRA, training loop, inference): 100%
- Backend/utilities: ≥80%
- Examples: Manual validation (not measured)
**CI Enforcement**: PR fails if coverage drops below 80%
## Rust Style Guidelines
### Error Handling
**Use `thiserror` for library errors** (not `anyhow`):
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ModelError {
#[error("model file not found: {path}")]
FileNotFound { path: PathBuf },
#[error("invalid model format: {reason}")]
InvalidFormat { reason: String },
#[error("incompatible model version: expected {expected}, found {found}")]
IncompatibleVersion { expected: String, found: String },
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
```
**Reserve `anyhow` for**:
- Examples only
- Tests only
- Never in library code
**Never use**:
- `.unwrap()` - Use `?` or explicit error handling
- `.expect()` - Only in tests or examples with clear justification
### API Design Patterns
**1. Builder Pattern for Complex Configuration**:
```rust
pub struct ModelLoader {
device: Device,
dtype: DType,
// ...
}
impl ModelLoader {
pub fn new() -> Self {
Self {
device: Device::metal(0).unwrap(), // Default
dtype: DType::F16,
}
}
pub fn with_device(mut self, device: Device) -> Self {
self.device = device;
self
}
pub fn with_dtype(mut self, dtype: DType) -> Self {
self.dtype = dtype;
self
}
pub fn load(self, path: impl AsRef<Path>) -> Result<Model, ModelError> {
// ...
}
}
```
**2. Type-Safe Configuration Structs**:
```rust
#[derive(Debug, Clone)]
pub struct TrainingConfig {
pub learning_rate: f32,
pub batch_size: usize,
pub num_epochs: usize,
pub warmup_steps: usize,
}
impl Default for TrainingConfig {
fn default() -> Self {
Self {
learning_rate: 1e-4,
batch_size: 4,
num_epochs: 3,
warmup_steps: 100,
}
}
}
```
**3. Newtype Pattern for Clarity**:
```rust
#[derive(Debug, Clone, Copy)]
pub struct Rank(usize);
#[derive(Debug, Clone, Copy)]
pub struct Alpha(f32);
impl Rank {
pub fn new(rank: usize) -> Result<Self, LoRAError> {
if rank == 0 {
return Err(LoRAError::InvalidRank);
}
Ok(Self(rank))
}
}
```
### Documentation Standards
**Every public item MUST have**:
1. Summary (one sentence)
2. Description (what it does, when to use)
3. Examples (simple, runnable)
4. Errors section (if returns `Result`)
5. Panics section (if can panic)
**Template**:
```rust
/// Loads a model from the specified path.
///
/// Supports multiple formats (safetensors primary, GGUF in v1.1+).
/// Format is auto-detected based on file extension. The model is loaded
/// onto the specified device (Metal by default).
///
/// # Examples
///
/// ```no_run
/// use metal_candle::ModelLoader;
///
/// let model = ModelLoader::new()
/// .with_dtype(DType::F16)
/// .load("qwen2.5-coder.safetensors")?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// # Errors
///
/// Returns [`ModelError::FileNotFound`] if the path doesn't exist.
/// Returns [`ModelError::InvalidFormat`] if the file is corrupted or unsupported.
/// Returns [`ModelError::IncompatibleVersion`] if the model version is incompatible.
///
/// # Panics
///
/// This function does not panic.
pub fn load(&self, path: impl AsRef<Path>) -> Result<Model, ModelError> {
// Implementation
}
```
**Module-Level Documentation**:
```rust
//! Model loading and format handling.
//!
//! This module provides utilities for loading ML models from various formats.
//! The primary format is safetensors, with extensibility for GGUF and others.
//!
//! # Examples
//!
//! ```no_run
//! use metal_candle::models::ModelLoader;
//!
//! let model = ModelLoader::new().load("model.safetensors")?;
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
```
## Testing Strategy
### Test Organization
```
tests/
├── integration/ # End-to-end workflows
│ ├── training.rs
│ └── inference.rs
├── models/ # Model-specific tests
│ └── loading.rs
└── backend/ # Backend tests
└── metal.rs
src/
└── module/
└── tests.rs # Unit tests inline with code
```
### Test Patterns
**1. Unit Tests (in same file)**:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lora_initialization() {
let lora = LoRALayer::new(512, 8).unwrap();
assert_eq!(lora.rank(), 8);
}
#[test]
fn test_invalid_rank_returns_error() {
let result = LoRALayer::new(512, 0);
assert!(matches!(result, Err(LoRAError::InvalidRank)));
}
}
```
**2. Property-Based Tests** (for numerical code):
```rust
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn test_softmax_sums_to_one(values in prop::collection::vec(-10.0f32..10.0, 1..100)) {
let result = softmax(&values);
let sum: f32 = result.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
}
}
}
```
**3. Integration Tests**:
```rust
// tests/integration/training.rs
use metal_candle::*;
#[test]
fn test_full_lora_training_workflow() {
// Load model
let model = ModelLoader::new().load("test_data/model.safetensors").unwrap();
// Create LoRA adapter
let lora = LoRAAdapter::new(&model, Rank::new(8).unwrap()).unwrap();
// Train
let config = TrainingConfig::default();
let trainer = Trainer::new(lora, config);
let trained = trainer.train(dataset).unwrap();
// Save checkpoint
trained.save("test_output/checkpoint.safetensors").unwrap();
}
```
**4. Snapshot Tests** (for model outputs):
```rust
#[cfg(test)]
mod snapshot_tests {
use insta::assert_debug_snapshot;
#[test]
fn test_model_forward_pass() {
let model = load_test_model();
let input = test_input();
let output = model.forward(input).unwrap();
// Snapshot the output shape and first few values
assert_debug_snapshot!(output.shape());
}
}
```
### Test Helpers
**Create test utilities in `tests/common/`**:
```rust
// tests/common/mod.rs
use metal_candle::*;
pub fn load_test_model() -> Model {
ModelLoader::new()
.load("tests/fixtures/small_model.safetensors")
.unwrap()
}
pub fn create_test_device() -> Device {
Device::metal(0).unwrap_or(Device::Cpu)
}
```
## Performance & Benchmarking
### Local Benchmarking (NOT in CI)
```bash
# Run benchmarks locally on Apple Silicon
cargo bench --bench mlx_comparison
# Compare against baseline
cargo bench --bench training -- --save-baseline main
```
### Local CI Testing
Test GitHub Actions workflows locally before pushing:
```bash
# Install act (if not already installed)
brew install act
# Run all CI jobs
act
# Run specific jobs
act -j clippy # Run clippy check
act -j test # Run test suite
act -j fmt # Run format check
# Dry run to see what would execute
act -n
```
**Note**: `act` uses Docker, so Metal-specific tests won't work. Use for:
- ✅ Clippy checks
- ✅ Format checks
- ✅ Build verification
- ❌ Not for: actual test execution, coverage, Metal operations
### Profiling Commands
```bash
# Memory profiling
cargo instruments -t Allocations --release --example train_lora
# CPU profiling
cargo instruments -t Time --release --example train_lora
# Metal GPU profiling
cargo instruments -t Metal --release --example train_lora
```
### Performance Targets
- Training: 90-100% of MLX+PyO3 throughput
- Inference: 95-100% of MLX+PyO3 speed
- Memory: ≤ MLX+PyO3 peak usage
- KV-cache: ≥2x speedup for long sequences
## ML-Specific Guidelines
### Tensor Operations
**Always specify device explicitly**:
```rust
// Good
let tensor = Tensor::zeros((batch, seq_len), DType::F32, &device)?;
// Bad
let tensor = Tensor::zeros((batch, seq_len))?; // Implicit device
```
**Use meaningful variable names**:
```rust
// Good
let attention_scores = query.matmul(&key.transpose())?;
let attention_weights = softmax(&attention_scores)?;
// Bad
let x = q.matmul(&k.t())?;
let y = softmax(&x)?;
```
### Numerical Stability
**Always consider numerical stability**:
```rust
// Good: Numerically stable softmax
pub fn softmax(x: &Tensor) -> Result<Tensor> {
let max = x.max_keepdim(-1)?;
let exp = (x.broadcast_sub(&max))?.exp()?;
let sum = exp.sum_keepdim(-1)?;
exp.broadcast_div(&sum)
}
// Bad: Naive implementation can overflow
pub fn softmax(x: &Tensor) -> Result<Tensor> {
let exp = x.exp()?; // Can overflow!
let sum = exp.sum_keepdim(-1)?;
exp.broadcast_div(&sum)
}
```
### Memory Management
**Always clean up tensors explicitly when dealing with large models**:
```rust
impl Model {
pub fn forward(&self, input: &Tensor) -> Result<Tensor> {
let hidden = self.embed(input)?;
// Process through layers
let mut current = hidden;
for layer in &self.layers {
let next = layer.forward(¤t)?;
drop(current); // Explicit cleanup of intermediate tensors
current = next;
}
Ok(current)
}
}
```
## Git Workflow
### Commit Messages
Follow Conventional Commits:
```
feat: add LoRA adapter implementation
fix: correct attention mask shape in Qwen model
docs: add examples for model loading
test: add integration tests for training pipeline
perf: optimize KV-cache memory usage
refactor: simplify tensor operation wrappers
```
### Pre-Commit Checklist
Before every commit:
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo test` passes
- [ ] `cargo fmt` applied
- [ ] New code has tests
- [ ] Public APIs have documentation
- [ ] No `unwrap()` or `expect()` in library code
**Optional**: Test CI locally before pushing:
```bash
act -j clippy # Verify clippy will pass in CI
act -j fmt # Verify formatting is correct
```
### Branch Strategy
- `main` - Always deployable, CI passing
- `phase-N-description` - Feature branches for each phase
- `fix/bug-description` - Bug fixes
- `docs/topic` - Documentation improvements
## Dependencies
### Allowed Dependencies
**Core**:
- `candle-core`, `candle-nn` - ML framework
- `safetensors` - Model format
- `tokenizers` - HuggingFace tokenizers
- `thiserror` - Error handling
- `tracing` - Logging
**Development**:
- `criterion` - Benchmarking
- `approx` - Float comparison
- `tempfile` - Test fixtures
- `proptest` - Property testing
- `insta` - Snapshot testing
### Dependency Rules
1. **Minimize dependencies** - Each new dep must be justified
2. **Pin versions** - Use exact versions for reproducibility
3. **No unnecessary features** - Only enable needed features
4. **Check licenses** - MIT/Apache-2.0 compatible only
5. **Audit regularly** - `cargo audit` before each release
## CI/CD
### CI Pipeline Requirements
```yaml
# .github/workflows/ci.yml
- Clippy (pedantic, zero warnings)
- Tests (all features, Apple Silicon)
- Code coverage (≥80% enforced)
- Documentation build
- Format check (cargo fmt)
```
### Release Checklist
Before publishing to crates.io:
- [ ] All tests passing
- [ ] Coverage ≥80%
- [ ] Documentation complete
- [ ] CHANGELOG.md updated
- [ ] Version bumped in Cargo.toml
- [ ] Examples tested manually
- [ ] Benchmarks run and documented
- [ ] README.md up to date
## File Organization
### Module Structure
```rust
// src/lib.rs - Public API exports only
pub mod error;
pub mod models;
pub mod training;
pub mod inference;
pub mod checkpoint;
// Re-exports for convenience
pub use error::{ModelError, TrainingError, InferenceError};
pub use models::ModelLoader;
pub use training::{LoRAAdapter, Trainer};
pub use inference::Generator;
```
### Internal vs Public
```rust
// Public API (in src/lib.rs or pub use)
pub struct ModelLoader { /* ... */ }
// Internal implementation (not re-exported)
mod backend {
pub(crate) struct MetalDevice { /* ... */ }
}
```
## Special Considerations
### Apple Silicon Specific
- **Always test on real Apple Silicon** - CI is approximation
- **Use Metal profiling tools** - Instruments.app for GPU profiling
- **Memory pressure** - Monitor with Activity Monitor during development
- **Thermal throttling** - Long benchmarks may throttle performance
### Candle Ecosystem
- **Stay up to date** - Monitor Candle releases for improvements
- **Contribute upstream** - Fix bugs in Candle when found
- **Community engagement** - Join Candle Discord for support
## Questions to Ask Before Implementing
1. **Is this the simplest solution that works?**
2. **How will I test this?**
3. **What can go wrong, and how do I handle it?**
4. **Is this documented well enough for external users?**
5. **Does this meet our performance targets?**
## Resources
- **Candle**: https://github.com/huggingface/candle
- **Rust API Guidelines**: https://rust-lang.github.io/api-guidelines/
- **LoRA Paper**: https://arxiv.org/abs/2106.09685
- **Project Plan**: See PLAN.md for full roadmap
---
**Remember**: This crate will be used by others. Every API, error message, and documentation comment matters. Ship production-quality code from day one.