red-candle 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +63 -0
- data/Rakefile +40 -0
- data/ext/candle/src/llm/quantized_gguf.rs +17 -1
- data/ext/candle/src/ruby/device.rs +2 -1
- data/ext/candle/src/ruby/dtype.rs +1 -0
- data/ext/candle/src/ruby/errors.rs +1 -0
- data/ext/candle/src/ruby/tensor.rs +2 -1
- data/ext/candle/src/tokenizer/mod.rs +2 -1
- data/ext/candle/tests/device_tests.rs +43 -0
- data/ext/candle/tests/tensor_tests.rs +162 -0
- data/lib/candle/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd566594df4f0d3ec8ecd592b1d71610ef0ba2091cd91ea53549405a8c5c9b18
|
4
|
+
data.tar.gz: cfaab403935e927371fbd2f605ea60da3f44effd64950bb132651a5e6437e30c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b18f384766db5a3de2f794764a8a47d3d69261ae4bf5c93992df7228fe9a5817eb1862b516a9ee3e63b5d349936f46534d50f1378b7932c7ac482550270a8bea
|
7
|
+
data.tar.gz: ce74834cde884bf03e3d163f6d081e87f95db0fba8db24300b8c6aa5b0f1f0162542c836bebb414010534778e394ca36faaec807ff1cc652902c3b318503def5
|
data/README.md
CHANGED
@@ -888,6 +888,69 @@ bundle exec rake compile
|
|
888
888
|
|
889
889
|
Pull requests are welcome.
|
890
890
|
|
891
|
+
## Testing
|
892
|
+
|
893
|
+
Red Candle has comprehensive tests at both the Ruby and Rust levels:
|
894
|
+
|
895
|
+
### Ruby Tests
|
896
|
+
```bash
|
897
|
+
# Run all Ruby tests
|
898
|
+
bundle exec rake test
|
899
|
+
|
900
|
+
# Run specific test suites
|
901
|
+
bundle exec rake test:device # Device compatibility tests
|
902
|
+
bundle exec rake test:benchmark # Benchmark tests
|
903
|
+
bundle exec rake test:llm:mistral # Model-specific tests
|
904
|
+
```
|
905
|
+
|
906
|
+
### Rust Tests
|
907
|
+
```bash
|
908
|
+
# Run Rust unit and integration tests
|
909
|
+
cd ext/candle && cargo test
|
910
|
+
|
911
|
+
# Or use the Rake task
|
912
|
+
bundle exec rake rust:test
|
913
|
+
```
|
914
|
+
|
915
|
+
The Rust tests include:
|
916
|
+
- Unit tests within source files (using `#[cfg(test)]` modules)
|
917
|
+
- Integration tests for external dependencies (candle_core operations)
|
918
|
+
- Tests for structured generation, tokenization, and text generation
|
919
|
+
|
920
|
+
### Code Coverage
|
921
|
+
|
922
|
+
#### Rust Code Coverage
|
923
|
+
Red Candle uses `cargo-llvm-cov` for Rust code coverage analysis:
|
924
|
+
|
925
|
+
```bash
|
926
|
+
# Generate HTML coverage report (opens in target/llvm-cov/html/index.html)
|
927
|
+
bundle exec rake rust:coverage:html
|
928
|
+
|
929
|
+
# Show coverage summary in terminal
|
930
|
+
bundle exec rake rust:coverage:summary
|
931
|
+
|
932
|
+
# Generate detailed coverage report
|
933
|
+
bundle exec rake rust:coverage:report
|
934
|
+
|
935
|
+
# Generate LCOV format for CI integration
|
936
|
+
bundle exec rake rust:coverage:lcov
|
937
|
+
|
938
|
+
# Clean coverage data
|
939
|
+
bundle exec rake rust:coverage:clean
|
940
|
+
```
|
941
|
+
|
942
|
+
**Note**: Overall Rust coverage shows ~17% because most code consists of Ruby FFI bindings that are tested through Ruby tests. The testable Rust components have high coverage:
|
943
|
+
- Constrained generation: 99.59%
|
944
|
+
- Schema processing: 90.99%
|
945
|
+
- Integration tests: 97.12%
|
946
|
+
|
947
|
+
#### Ruby Code Coverage
|
948
|
+
Ruby test coverage is generated automatically when running tests:
|
949
|
+
```bash
|
950
|
+
bundle exec rake test
|
951
|
+
# Coverage report generated in coverage/index.html
|
952
|
+
```
|
953
|
+
|
891
954
|
## Release
|
892
955
|
|
893
956
|
1. Update version number in `lib/candle/version.rb` and commit.
|
data/Rakefile
CHANGED
@@ -134,3 +134,43 @@ namespace :doc do
|
|
134
134
|
end
|
135
135
|
|
136
136
|
task doc: "doc:default"
|
137
|
+
|
138
|
+
namespace :rust do
|
139
|
+
desc "Run Rust tests with code coverage"
|
140
|
+
namespace :coverage do
|
141
|
+
desc "Generate HTML coverage report"
|
142
|
+
task :html do
|
143
|
+
sh "cd ext/candle && cargo llvm-cov --html"
|
144
|
+
puts "Coverage report generated in target/llvm-cov/html/index.html"
|
145
|
+
end
|
146
|
+
|
147
|
+
desc "Generate coverage report in terminal"
|
148
|
+
task :report do
|
149
|
+
sh "cd ext/candle && cargo llvm-cov"
|
150
|
+
end
|
151
|
+
|
152
|
+
desc "Show coverage summary"
|
153
|
+
task :summary do
|
154
|
+
sh "cd ext/candle && cargo llvm-cov --summary-only"
|
155
|
+
end
|
156
|
+
|
157
|
+
desc "Generate lcov format coverage report"
|
158
|
+
task :lcov do
|
159
|
+
sh "cd ext/candle && cargo llvm-cov --lcov --output-path ../../coverage/lcov.info"
|
160
|
+
puts "LCOV report generated in coverage/lcov.info"
|
161
|
+
end
|
162
|
+
|
163
|
+
desc "Clean coverage data"
|
164
|
+
task :clean do
|
165
|
+
sh "cd ext/candle && cargo llvm-cov clean"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
desc "Run Rust tests"
|
170
|
+
task :test do
|
171
|
+
sh "cd ext/candle && cargo test"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
desc "Run Rust tests with coverage (alias)"
|
176
|
+
task "coverage:rust" => "rust:coverage:html"
|
@@ -320,7 +320,9 @@ impl QuantizedGGUF {
|
|
320
320
|
// Check model name since Mistral GGUF reports as llama architecture
|
321
321
|
let model_lower = self.model_id.to_lowercase();
|
322
322
|
|
323
|
-
if model_lower.contains("
|
323
|
+
if model_lower.contains("tinyllama") {
|
324
|
+
self.apply_chatml_template(messages)
|
325
|
+
} else if model_lower.contains("mistral") {
|
324
326
|
self.apply_mistral_template(messages)
|
325
327
|
} else if model_lower.contains("gemma") {
|
326
328
|
// Always use Gemma template for Gemma models, regardless of loader used
|
@@ -516,6 +518,20 @@ impl QuantizedGGUF {
|
|
516
518
|
Ok(prompt)
|
517
519
|
}
|
518
520
|
|
521
|
+
fn apply_chatml_template(&self, messages: &[serde_json::Value]) -> CandleResult<String> {
|
522
|
+
let mut prompt = String::new();
|
523
|
+
|
524
|
+
for message in messages {
|
525
|
+
let role = message["role"].as_str().unwrap_or("");
|
526
|
+
let content = message["content"].as_str().unwrap_or("");
|
527
|
+
|
528
|
+
prompt.push_str(&format!("<|{}|>\n{}</s>\n", role, content));
|
529
|
+
}
|
530
|
+
|
531
|
+
prompt.push_str("<|assistant|>");
|
532
|
+
Ok(prompt)
|
533
|
+
}
|
534
|
+
|
519
535
|
fn apply_generic_template(&self, messages: &[serde_json::Value]) -> String {
|
520
536
|
let mut prompt = String::new();
|
521
537
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
use candle_core::Device as CoreDevice;
|
2
|
+
|
3
|
+
#[test]
|
4
|
+
fn test_device_creation() {
|
5
|
+
// CPU device should always work
|
6
|
+
let cpu = CoreDevice::Cpu;
|
7
|
+
assert!(matches!(cpu, CoreDevice::Cpu));
|
8
|
+
|
9
|
+
// Test device display
|
10
|
+
assert_eq!(format!("{:?}", cpu), "Cpu");
|
11
|
+
}
|
12
|
+
|
13
|
+
#[cfg(feature = "cuda")]
|
14
|
+
#[test]
|
15
|
+
#[ignore = "requires CUDA hardware"]
|
16
|
+
fn test_cuda_device_creation() {
|
17
|
+
// This might fail if no CUDA device is available
|
18
|
+
match CoreDevice::new_cuda(0) {
|
19
|
+
Ok(device) => assert!(matches!(device, CoreDevice::Cuda(_))),
|
20
|
+
Err(_) => println!("No CUDA device available for testing"),
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
#[cfg(feature = "metal")]
|
25
|
+
#[test]
|
26
|
+
#[ignore = "requires Metal hardware"]
|
27
|
+
fn test_metal_device_creation() {
|
28
|
+
// This might fail if no Metal device is available
|
29
|
+
match CoreDevice::new_metal(0) {
|
30
|
+
Ok(device) => assert!(matches!(device, CoreDevice::Metal(_))),
|
31
|
+
Err(_) => println!("No Metal device available for testing"),
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
#[test]
|
36
|
+
fn test_device_matching() {
|
37
|
+
let cpu1 = CoreDevice::Cpu;
|
38
|
+
let cpu2 = CoreDevice::Cpu;
|
39
|
+
|
40
|
+
// Same device types should match
|
41
|
+
assert!(matches!(cpu1, CoreDevice::Cpu));
|
42
|
+
assert!(matches!(cpu2, CoreDevice::Cpu));
|
43
|
+
}
|
@@ -0,0 +1,162 @@
|
|
1
|
+
use candle_core::{Tensor, Device, DType};
|
2
|
+
|
3
|
+
#[test]
|
4
|
+
fn test_tensor_creation() {
|
5
|
+
let device = Device::Cpu;
|
6
|
+
|
7
|
+
// Test tensor creation from slice
|
8
|
+
let data = vec![1.0f32, 2.0, 3.0, 4.0];
|
9
|
+
let tensor = Tensor::new(&data[..], &device).unwrap();
|
10
|
+
assert_eq!(tensor.dims(), &[4]);
|
11
|
+
assert_eq!(tensor.dtype(), DType::F32);
|
12
|
+
|
13
|
+
// Test zeros
|
14
|
+
let zeros = Tensor::zeros(&[2, 3], DType::F32, &device).unwrap();
|
15
|
+
assert_eq!(zeros.dims(), &[2, 3]);
|
16
|
+
|
17
|
+
// Test ones
|
18
|
+
let ones = Tensor::ones(&[3, 2], DType::F32, &device).unwrap();
|
19
|
+
assert_eq!(ones.dims(), &[3, 2]);
|
20
|
+
}
|
21
|
+
|
22
|
+
#[test]
|
23
|
+
fn test_tensor_arithmetic() {
|
24
|
+
let device = Device::Cpu;
|
25
|
+
|
26
|
+
let a = Tensor::new(&[1.0f32, 2.0, 3.0], &device).unwrap();
|
27
|
+
let b = Tensor::new(&[4.0f32, 5.0, 6.0], &device).unwrap();
|
28
|
+
|
29
|
+
// Addition
|
30
|
+
let sum = a.add(&b).unwrap();
|
31
|
+
let sum_vec: Vec<f32> = sum.to_vec1().unwrap();
|
32
|
+
assert_eq!(sum_vec, vec![5.0, 7.0, 9.0]);
|
33
|
+
|
34
|
+
// Subtraction
|
35
|
+
let diff = a.sub(&b).unwrap();
|
36
|
+
let diff_vec: Vec<f32> = diff.to_vec1().unwrap();
|
37
|
+
assert_eq!(diff_vec, vec![-3.0, -3.0, -3.0]);
|
38
|
+
|
39
|
+
// Multiplication
|
40
|
+
let prod = a.mul(&b).unwrap();
|
41
|
+
let prod_vec: Vec<f32> = prod.to_vec1().unwrap();
|
42
|
+
assert_eq!(prod_vec, vec![4.0, 10.0, 18.0]);
|
43
|
+
}
|
44
|
+
|
45
|
+
#[test]
|
46
|
+
fn test_tensor_reshape() {
|
47
|
+
let device = Device::Cpu;
|
48
|
+
|
49
|
+
let tensor = Tensor::new(&[1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], &device).unwrap();
|
50
|
+
|
51
|
+
// Reshape to 2x3
|
52
|
+
let reshaped = tensor.reshape(&[2, 3]).unwrap();
|
53
|
+
assert_eq!(reshaped.dims(), &[2, 3]);
|
54
|
+
|
55
|
+
// Reshape to 3x2
|
56
|
+
let reshaped = tensor.reshape(&[3, 2]).unwrap();
|
57
|
+
assert_eq!(reshaped.dims(), &[3, 2]);
|
58
|
+
}
|
59
|
+
|
60
|
+
#[test]
|
61
|
+
fn test_tensor_transpose() {
|
62
|
+
let device = Device::Cpu;
|
63
|
+
|
64
|
+
let tensor = Tensor::new(&[1.0f32, 2.0, 3.0, 4.0], &device)
|
65
|
+
.unwrap()
|
66
|
+
.reshape(&[2, 2])
|
67
|
+
.unwrap();
|
68
|
+
|
69
|
+
let transposed = tensor.transpose(0, 1).unwrap();
|
70
|
+
assert_eq!(transposed.dims(), &[2, 2]);
|
71
|
+
|
72
|
+
let values: Vec<f32> = transposed.flatten_all().unwrap().to_vec1().unwrap();
|
73
|
+
assert_eq!(values, vec![1.0, 3.0, 2.0, 4.0]);
|
74
|
+
}
|
75
|
+
|
76
|
+
#[test]
|
77
|
+
fn test_tensor_reduction() {
|
78
|
+
let device = Device::Cpu;
|
79
|
+
|
80
|
+
let tensor = Tensor::new(&[1.0f32, 2.0, 3.0, 4.0], &device).unwrap();
|
81
|
+
|
82
|
+
// Sum
|
83
|
+
let sum = tensor.sum_all().unwrap();
|
84
|
+
let sum_val: f32 = sum.to_scalar().unwrap();
|
85
|
+
assert_eq!(sum_val, 10.0);
|
86
|
+
|
87
|
+
// Mean
|
88
|
+
let mean = tensor.mean_all().unwrap();
|
89
|
+
let mean_val: f32 = mean.to_scalar().unwrap();
|
90
|
+
assert_eq!(mean_val, 2.5);
|
91
|
+
}
|
92
|
+
|
93
|
+
#[test]
|
94
|
+
fn test_tensor_indexing() {
|
95
|
+
let device = Device::Cpu;
|
96
|
+
|
97
|
+
let tensor = Tensor::new(&[10.0f32, 20.0, 30.0, 40.0], &device).unwrap();
|
98
|
+
|
99
|
+
// Get element at index 0
|
100
|
+
let elem = tensor.get(0).unwrap();
|
101
|
+
let val: f32 = elem.to_scalar().unwrap();
|
102
|
+
assert_eq!(val, 10.0);
|
103
|
+
|
104
|
+
// Get element at index 2
|
105
|
+
let elem = tensor.get(2).unwrap();
|
106
|
+
let val: f32 = elem.to_scalar().unwrap();
|
107
|
+
assert_eq!(val, 30.0);
|
108
|
+
}
|
109
|
+
|
110
|
+
#[test]
|
111
|
+
fn test_tensor_matmul() {
|
112
|
+
let device = Device::Cpu;
|
113
|
+
|
114
|
+
// 2x3 matrix
|
115
|
+
let a = Tensor::new(&[1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], &device)
|
116
|
+
.unwrap()
|
117
|
+
.reshape(&[2, 3])
|
118
|
+
.unwrap();
|
119
|
+
|
120
|
+
// 3x2 matrix
|
121
|
+
let b = Tensor::new(&[7.0f32, 8.0, 9.0, 10.0, 11.0, 12.0], &device)
|
122
|
+
.unwrap()
|
123
|
+
.reshape(&[3, 2])
|
124
|
+
.unwrap();
|
125
|
+
|
126
|
+
// Matrix multiplication
|
127
|
+
let result = a.matmul(&b).unwrap();
|
128
|
+
assert_eq!(result.dims(), &[2, 2]);
|
129
|
+
|
130
|
+
let values: Vec<f32> = result.flatten_all().unwrap().to_vec1().unwrap();
|
131
|
+
// [1*7 + 2*9 + 3*11, 1*8 + 2*10 + 3*12, 4*7 + 5*9 + 6*11, 4*8 + 5*10 + 6*12]
|
132
|
+
// = [58, 64, 139, 154]
|
133
|
+
assert_eq!(values, vec![58.0, 64.0, 139.0, 154.0]);
|
134
|
+
}
|
135
|
+
|
136
|
+
#[test]
|
137
|
+
fn test_tensor_where() {
|
138
|
+
let device = Device::Cpu;
|
139
|
+
|
140
|
+
// Create a condition tensor where values > 0 are treated as true
|
141
|
+
let cond_values = Tensor::new(&[1.0f32, 0.0, 1.0], &device).unwrap();
|
142
|
+
let cond = cond_values.gt(&Tensor::zeros(cond_values.shape(), DType::F32, &device).unwrap()).unwrap();
|
143
|
+
|
144
|
+
let on_true = Tensor::new(&[10.0f32, 20.0, 30.0], &device).unwrap();
|
145
|
+
let on_false = Tensor::new(&[100.0f32, 200.0, 300.0], &device).unwrap();
|
146
|
+
|
147
|
+
let result = cond.where_cond(&on_true, &on_false).unwrap();
|
148
|
+
let values: Vec<f32> = result.to_vec1().unwrap();
|
149
|
+
assert_eq!(values, vec![10.0, 200.0, 30.0]);
|
150
|
+
}
|
151
|
+
|
152
|
+
#[test]
|
153
|
+
fn test_tensor_narrow() {
|
154
|
+
let device = Device::Cpu;
|
155
|
+
|
156
|
+
let tensor = Tensor::new(&[1.0f32, 2.0, 3.0, 4.0, 5.0], &device).unwrap();
|
157
|
+
|
158
|
+
// Narrow from index 1, length 3
|
159
|
+
let narrowed = tensor.narrow(0, 1, 3).unwrap();
|
160
|
+
let values: Vec<f32> = narrowed.to_vec1().unwrap();
|
161
|
+
assert_eq!(values, vec![2.0, 3.0, 4.0]);
|
162
|
+
}
|
data/lib/candle/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: red-candle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Petersen
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2025-
|
12
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rb_sys
|
@@ -196,6 +196,8 @@ files:
|
|
196
196
|
- ext/candle/target/release/build/clang-sys-cac31d63c4694603/out/macros.rs
|
197
197
|
- ext/candle/target/release/build/pulp-1b95cfe377eede97/out/x86_64_asm.rs
|
198
198
|
- ext/candle/target/release/build/rb-sys-f8ac4edc30ab3e53/out/bindings-0.9.116-mri-arm64-darwin24-3.3.0.rs
|
199
|
+
- ext/candle/tests/device_tests.rs
|
200
|
+
- ext/candle/tests/tensor_tests.rs
|
199
201
|
- lib/candle.rb
|
200
202
|
- lib/candle/build_info.rb
|
201
203
|
- lib/candle/device_utils.rb
|