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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3678037fbb196c621c8e9df6a213b0d3dffbdb1b8b3dfd73eee4a7ea2feafca
4
- data.tar.gz: ada97ef81af854439622bdc12b796442be9e0f31e7c7d8a5df374c7bfb07ff2e
3
+ metadata.gz: cd566594df4f0d3ec8ecd592b1d71610ef0ba2091cd91ea53549405a8c5c9b18
4
+ data.tar.gz: cfaab403935e927371fbd2f605ea60da3f44effd64950bb132651a5e6437e30c
5
5
  SHA512:
6
- metadata.gz: d353177318c4599fa30974a676350087a8e5fd070fe3d317344a4e1b3ae022cb69adf742d62063c2da09dbab7e971cbfae1e53a87527ce7f1c18afd1223797e8
7
- data.tar.gz: df4b2f43f6fb1aa623053fd09d6e48eba0d8c2615f51dc2accdc4dc292fb3fb7d665553b04cae3747e001e04cd4b9cdbe5c022c3efd077daddf97e074a1e9e5c
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("mistral") {
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
 
@@ -199,4 +199,5 @@ pub fn init(rb_candle: RModule) -> Result<()> {
199
199
  rb_device.define_method("inspect", method!(Device::__repr__, 0))?;
200
200
  rb_device.define_method("==", method!(Device::__eq__, 1))?;
201
201
  Ok(())
202
- }
202
+ }
203
+
@@ -35,3 +35,4 @@ pub fn init(rb_candle: RModule) -> Result<()> {
35
35
  rb_dtype.define_method("inspect", method!(DType::__repr__, 0))?;
36
36
  Ok(())
37
37
  }
38
+
@@ -11,3 +11,4 @@ pub fn wrap_candle_err(err: candle_core::Error) -> Error {
11
11
  pub fn wrap_hf_err(err: hf_hub::api::sync::ApiError) -> Error {
12
12
  Error::new(magnus::exception::runtime_error(), err.to_string())
13
13
  }
14
+
@@ -651,4 +651,5 @@ pub fn init(rb_candle: RModule) -> Result<()> {
651
651
  rb_tensor.define_method("to_s", method!(Tensor::__str__, 0))?;
652
652
  rb_tensor.define_method("inspect", method!(Tensor::__repr__, 0))?;
653
653
  Ok(())
654
- }
654
+ }
655
+
@@ -100,4 +100,5 @@ impl TokenizerWrapper {
100
100
  pub fn inner_mut(&mut self) -> &mut Tokenizer {
101
101
  &mut self.tokenizer
102
102
  }
103
- }
103
+ }
104
+
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  # :nocov:
2
2
  module Candle
3
- VERSION = "1.1.1"
3
+ VERSION = "1.1.2"
4
4
  end
5
5
  # :nocov:
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.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-07-28 00:00:00.000000000 Z
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