trainers-rb 0.1.0 → 0.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/CHANGELOG.md +30 -0
- data/README.md +26 -5
- data/lib/trainers/lora/lora_linear.rb +25 -1
- data/lib/trainers/lora/lora_model.rb +6 -8
- data/lib/trainers/training_arguments.rb +42 -2
- data/lib/trainers/version.rb +1 -1
- metadata +15 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2025ca407a70ba9d0a206306f48486eaae88854e767dd006aa9f37af752eaf1
|
|
4
|
+
data.tar.gz: 5e4a9e5a90d377791c5577399b88aab55e5d00468f4cae13710e0d140d482e5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16679778184715a50d08ff4de4e5050bca75c98dc02b542331f14aae79a6a0b857b3a2699da060e3de3ce7924357486b3b60da2563e7759eccd4e2fd10d6c716
|
|
7
|
+
data.tar.gz: d1580d3571d6adaa1dd771b4cfdbea1bf9ea3a002b35bb06e5f4f9184946a6243dd9aac19c4ac96fccb19690e1e7c2291d51a0c03ad551f7ca32600a7092cc91
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 (2026-10-30)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `LoraModel.merge` no longer applies adapters twice. Merged layers kept adding the LoRA path on top of the merged weights, so a merged model's outputs were wrong. Each `LoraLinear` is now replaced by a plain `Torch::NN::Linear` holding the merged weights, so a saved merged model has the base model's parameter names. `LoraLinear#merge!` is idempotent and adds `#merged?` and `#to_linear`.
|
|
8
|
+
- `Trainers::VERSION` now matches the gem version. The gemspec reads it from `lib/trainers/version.rb`. (#2)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `TrainingArguments` validates its values at construction, and raises `ArgumentError` naming the argument, the value received and what was expected (#8):
|
|
13
|
+
- positive: `learning_rate`
|
|
14
|
+
- positive Integer: `num_train_epochs`, both batch sizes, `gradient_accumulation_steps`, `logging_steps`, `save_steps`
|
|
15
|
+
- non-negative: `weight_decay`, `max_grad_norm`; `warmup_steps` must also be an Integer
|
|
16
|
+
- `warmup_ratio` must be within 0..1, and `eval_steps` must be nil or a positive Integer
|
|
17
|
+
- `lr_scheduler_type` must be one of `:linear`, `:cosine`, `:constant`; `eval_strategy` and `save_strategy` one of `:no`, `:epoch`, `:steps`
|
|
18
|
+
|
|
19
|
+
Previously these were accepted and failed later, or were silently ignored. Defaults are unchanged.
|
|
20
|
+
|
|
21
|
+
### Internal
|
|
22
|
+
|
|
23
|
+
- CI on Ruby 3.3, 3.4 and 4.0 against torch-rb 0.17 (LibTorch 2.4.0) and 0.27 (LibTorch 2.15.0), using Appraisal gemfiles, plus macOS arm64; every job is required (#9)
|
|
24
|
+
- Tests for LR schedulers and warmup (#6), callback order and arguments (#7), LoRA shapes, identity init, merge and adapter round-trip (#5), and a public API surface test (#10)
|
|
25
|
+
- Development dependency minitest bumped to ~> 6.0
|
|
26
|
+
- CONTRIBUTING.md with LibTorch setup, SECURITY.md, README badges and a known-limitations section (#11)
|
|
27
|
+
|
|
28
|
+
## 0.1.1 (2026-06-09)
|
|
29
|
+
|
|
30
|
+
- Fix gemspec homepage, source and changelog URLs to point to github.com/Selectus2/trainers-rb
|
|
31
|
+
- No code changes. The published gem reports `Trainers::VERSION` as "0.1.0"; the constant is corrected here
|
|
32
|
+
|
|
3
33
|
## 0.1.0 (2026-06-07)
|
|
4
34
|
|
|
5
35
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# trainers-rb
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/trainers-rb)
|
|
4
|
+
[](https://github.com/Selectus2/trainers-rb/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
[](.github/workflows/ci.yml)
|
|
7
|
+
|
|
3
8
|
Fine-tune transformer models in Ruby.
|
|
4
9
|
|
|
5
10
|
trainers-rb provides a training loop, LoRA (Low-Rank Adaptation), learning rate scheduling, and model serialization for HuggingFace transformer models loaded via [transformers-rb](https://github.com/ankane/transformers-ruby). It builds on [torch-rb](https://github.com/ankane/torch.rb) for autograd, optimizers, and tensor operations.
|
|
@@ -22,15 +27,16 @@ gem install trainers-rb
|
|
|
22
27
|
|
|
23
28
|
### Prerequisites
|
|
24
29
|
|
|
25
|
-
trainers-rb depends on torch-rb, which
|
|
30
|
+
trainers-rb depends on torch-rb, which compiles against LibTorch. Use the LibTorch version that matches your torch-rb: 2.15.0 for torch-rb 0.27, or 2.4.0 for torch-rb 0.17.
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
33
|
# macOS arm64
|
|
29
|
-
curl -L -o /tmp/libtorch.zip https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-2.
|
|
30
|
-
unzip /tmp/libtorch.zip -d ~/libtorch
|
|
31
|
-
bundle config set build.torch-rb --with-torch-dir=$HOME/libtorch/libtorch
|
|
34
|
+
curl -L -o /tmp/libtorch.zip https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-2.15.0.zip
|
|
35
|
+
unzip /tmp/libtorch.zip -d ~/libtorch-2.15.0
|
|
36
|
+
bundle config set build.torch-rb --with-torch-dir=$HOME/libtorch-2.15.0/libtorch
|
|
32
37
|
```
|
|
33
38
|
|
|
39
|
+
For Linux, and for running the tests, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
34
40
|
## Quick Start
|
|
35
41
|
|
|
36
42
|
```ruby
|
|
@@ -140,6 +146,8 @@ Trainers::LoraModel.load_adapters(model, "./lora-adapters")
|
|
|
140
146
|
| `seed` | `42` | Random seed |
|
|
141
147
|
| `no_mps` | `false` | Force CPU even if MPS is available |
|
|
142
148
|
|
|
149
|
+
Invalid values, such as a negative learning rate, a zero batch size or an unknown `lr_scheduler_type`, raise `ArgumentError` when `TrainingArguments` is created.
|
|
150
|
+
|
|
143
151
|
## Callbacks
|
|
144
152
|
|
|
145
153
|
Built-in callbacks:
|
|
@@ -274,6 +282,19 @@ trainers-rb
|
|
|
274
282
|
|
|
275
283
|
trainers-rb adds the training layer that transformers-rb intentionally omits. Both gems call into the same LibTorch C++ kernels for the actual computation.
|
|
276
284
|
|
|
285
|
+
## Known limitations
|
|
286
|
+
|
|
287
|
+
These are open correctness issues. They are tracked, and listed here so nobody discovers them the hard way:
|
|
288
|
+
|
|
289
|
+
- **The learning-rate schedule is wrong with `gradient_accumulation_steps > 1`.** The scheduler is sized in micro-batches but stepped once per optimizer step, so it never decays fully. ([#12](https://github.com/Selectus2/trainers-rb/issues/12))
|
|
290
|
+
- **`seed:` is never applied.** Runs are not reproducible. ([#13](https://github.com/Selectus2/trainers-rb/issues/13))
|
|
291
|
+
- **`dataloader_drop_last:` and `label_names:` are accepted and ignored.** ([#14](https://github.com/Selectus2/trainers-rb/issues/14))
|
|
292
|
+
- **Progress output goes to stdout through `puts`.** There is no logger to configure or silence. ([#15](https://github.com/Selectus2/trainers-rb/issues/15))
|
|
293
|
+
- **`save_lora_adapters` returns `nil` instead of raising when a model has no adapters.** ([#16](https://github.com/Selectus2/trainers-rb/issues/16))
|
|
294
|
+
- **A saved model cannot be loaded back.** No `config.json` is written. ([#17](https://github.com/Selectus2/trainers-rb/issues/17))
|
|
295
|
+
- **Checkpoints cannot resume training.** Optimizer, scheduler and trainer state are not saved. ([#18](https://github.com/Selectus2/trainers-rb/issues/18))
|
|
296
|
+
- **LoRA checkpoints save the full state dict** rather than just the adapters. ([#19](https://github.com/Selectus2/trainers-rb/issues/19))
|
|
297
|
+
|
|
277
298
|
## Roadmap
|
|
278
299
|
|
|
279
300
|
- [ ] More model architectures in transformers-rb (GPT-2, Llama for text generation)
|
|
@@ -286,7 +307,7 @@ trainers-rb adds the training layer that transformers-rb intentionally omits. Bo
|
|
|
286
307
|
|
|
287
308
|
## Contributing
|
|
288
309
|
|
|
289
|
-
Bug reports and pull requests are welcome
|
|
310
|
+
Bug reports and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers LibTorch setup, running the tests against each supported torch-rb, and what a good PR looks like. Report security issues privately, as described in [SECURITY.md](SECURITY.md).
|
|
290
311
|
|
|
291
312
|
## License
|
|
292
313
|
|
|
@@ -36,12 +36,20 @@ module Trainers
|
|
|
36
36
|
@lora_B = Torch::NN::Parameter.new(Torch.zeros(@out_features, @r))
|
|
37
37
|
|
|
38
38
|
@lora_dropout = lora_dropout > 0 ? Torch::NN::Dropout.new(p: lora_dropout) : nil
|
|
39
|
+
@merged = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def merged?
|
|
43
|
+
@merged
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def forward(x)
|
|
42
47
|
# Original linear
|
|
43
48
|
base_output = Torch::NN::F.linear(x, @weight, @bias)
|
|
44
49
|
|
|
50
|
+
# After merge! the adapter is already folded into @weight
|
|
51
|
+
return base_output if @merged
|
|
52
|
+
|
|
45
53
|
# LoRA path
|
|
46
54
|
lora_input = @lora_dropout ? @lora_dropout.call(x) : x
|
|
47
55
|
lora_output = lora_input.matmul(@lora_A.t).matmul(@lora_B.t) * @scaling
|
|
@@ -49,14 +57,30 @@ module Trainers
|
|
|
49
57
|
base_output + lora_output
|
|
50
58
|
end
|
|
51
59
|
|
|
52
|
-
# Merge LoRA weights into the base weight matrix (for inference)
|
|
60
|
+
# Merge LoRA weights into the base weight matrix (for inference).
|
|
61
|
+
# Idempotent: merging twice would add the adapter twice.
|
|
53
62
|
def merge!
|
|
63
|
+
return self if @merged
|
|
64
|
+
|
|
54
65
|
Torch.no_grad do
|
|
55
66
|
@weight.add!(@lora_B.matmul(@lora_A) * @scaling)
|
|
56
67
|
end
|
|
68
|
+
@merged = true
|
|
57
69
|
self
|
|
58
70
|
end
|
|
59
71
|
|
|
72
|
+
# A plain Linear holding the merged weights, so a merged model has the
|
|
73
|
+
# same parameter names as the base model
|
|
74
|
+
def to_linear
|
|
75
|
+
merge!
|
|
76
|
+
linear = Torch::NN::Linear.new(@in_features, @out_features, bias: !@bias.nil?)
|
|
77
|
+
Torch.no_grad do
|
|
78
|
+
linear.weight.copy!(@weight)
|
|
79
|
+
linear.bias.copy!(@bias) if @bias
|
|
80
|
+
end
|
|
81
|
+
linear
|
|
82
|
+
end
|
|
83
|
+
|
|
60
84
|
# Extract LoRA adapter state for saving
|
|
61
85
|
def lora_state_dict
|
|
62
86
|
{ "lora_A" => @lora_A.data, "lora_B" => @lora_B.data }
|
|
@@ -56,16 +56,14 @@ module Trainers
|
|
|
56
56
|
model
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Merge all LoRA weights back into base weights (for inference)
|
|
59
|
+
# Merge all LoRA weights back into base weights (for inference).
|
|
60
|
+
# Each LoraLinear is replaced by a plain Linear with the merged weights.
|
|
60
61
|
def self.merge(model)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
mod.merge!
|
|
65
|
-
count += 1
|
|
66
|
-
end
|
|
62
|
+
lora_modules = model.named_modules.select { |_, mod| mod.is_a?(LoraLinear) }
|
|
63
|
+
lora_modules.each do |name, mod|
|
|
64
|
+
LoraUtils.replace_module(model, name, mod.to_linear)
|
|
67
65
|
end
|
|
68
|
-
puts "Merged #{
|
|
66
|
+
puts "Merged #{lora_modules.size} LoRA adapters into base model"
|
|
69
67
|
model
|
|
70
68
|
end
|
|
71
69
|
|
|
@@ -31,18 +31,28 @@ module Trainers
|
|
|
31
31
|
label_names: ["labels"]
|
|
32
32
|
}.freeze
|
|
33
33
|
|
|
34
|
+
SCHEDULER_TYPES = %i[linear cosine constant].freeze
|
|
35
|
+
STRATEGIES = %i[no epoch steps].freeze
|
|
36
|
+
|
|
37
|
+
POSITIVE_NUMBERS = %i[learning_rate].freeze
|
|
38
|
+
POSITIVE_INTEGERS = %i[num_train_epochs per_device_train_batch_size per_device_eval_batch_size
|
|
39
|
+
gradient_accumulation_steps logging_steps save_steps].freeze
|
|
40
|
+
NON_NEGATIVE = %i[weight_decay max_grad_norm].freeze
|
|
41
|
+
|
|
34
42
|
DEFAULTS.each_key do |key|
|
|
35
43
|
attr_accessor key
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
def initialize(**kwargs)
|
|
47
|
+
unknown = kwargs.keys - DEFAULTS.keys
|
|
48
|
+
raise ArgumentError, "Unknown arguments: #{unknown.join(', ')}" unless unknown.empty?
|
|
49
|
+
|
|
39
50
|
DEFAULTS.each do |key, default|
|
|
40
51
|
value = kwargs.fetch(key, default)
|
|
41
52
|
instance_variable_set(:"@#{key}", value)
|
|
42
53
|
end
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
raise ArgumentError, "Unknown arguments: #{unknown.join(', ')}" unless unknown.empty?
|
|
55
|
+
validate!
|
|
46
56
|
end
|
|
47
57
|
|
|
48
58
|
def resolved_device
|
|
@@ -60,5 +70,35 @@ module Trainers
|
|
|
60
70
|
hash[key] = send(key)
|
|
61
71
|
end
|
|
62
72
|
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Rejects values that cannot produce a sensible run. Errors name the
|
|
77
|
+
# argument, the value received, and what was expected.
|
|
78
|
+
def validate!
|
|
79
|
+
POSITIVE_NUMBERS.each do |key|
|
|
80
|
+
check(key, "a positive number") { |v| v.is_a?(Numeric) && v.positive? }
|
|
81
|
+
end
|
|
82
|
+
POSITIVE_INTEGERS.each do |key|
|
|
83
|
+
check(key, "a positive Integer") { |v| v.is_a?(Integer) && v.positive? }
|
|
84
|
+
end
|
|
85
|
+
NON_NEGATIVE.each do |key|
|
|
86
|
+
check(key, "a non-negative number") { |v| v.is_a?(Numeric) && !v.negative? }
|
|
87
|
+
end
|
|
88
|
+
check(:warmup_steps, "a non-negative Integer") { |v| v.is_a?(Integer) && !v.negative? }
|
|
89
|
+
check(:warmup_ratio, "a number between 0 and 1") { |v| v.is_a?(Numeric) && v.between?(0, 1) }
|
|
90
|
+
check(:eval_steps, "nil or a positive Integer") { |v| v.nil? || (v.is_a?(Integer) && v.positive?) }
|
|
91
|
+
|
|
92
|
+
check(:lr_scheduler_type, "one of #{SCHEDULER_TYPES.map(&:inspect).join(', ')}") { |v| SCHEDULER_TYPES.include?(v) }
|
|
93
|
+
check(:eval_strategy, "one of #{STRATEGIES.map(&:inspect).join(', ')}") { |v| STRATEGIES.include?(v) }
|
|
94
|
+
check(:save_strategy, "one of #{STRATEGIES.map(&:inspect).join(', ')}") { |v| STRATEGIES.include?(v) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check(key, expected)
|
|
98
|
+
value = send(key)
|
|
99
|
+
return if yield(value)
|
|
100
|
+
|
|
101
|
+
raise ArgumentError, "#{key} must be #{expected}, got #{value.inspect}"
|
|
102
|
+
end
|
|
63
103
|
end
|
|
64
104
|
end
|
data/lib/trainers/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trainers-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vishwajeetsingh Desurkar
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: torch-rb
|
|
@@ -85,20 +86,20 @@ dependencies:
|
|
|
85
86
|
requirements:
|
|
86
87
|
- - "~>"
|
|
87
88
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '
|
|
89
|
+
version: '6.0'
|
|
89
90
|
type: :development
|
|
90
91
|
prerelease: false
|
|
91
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
93
|
requirements:
|
|
93
94
|
- - "~>"
|
|
94
95
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '
|
|
96
|
+
version: '6.0'
|
|
96
97
|
description: Training loop, LoRA, and optimization utilities for fine-tuning HuggingFace
|
|
97
98
|
transformer models using torch-rb and transformers-rb. Supports full fine-tuning,
|
|
98
99
|
LoRA adapters, learning rate scheduling, callbacks, and model serialization via
|
|
99
100
|
safetensors.
|
|
100
101
|
email:
|
|
101
|
-
-
|
|
102
|
+
- vishwajeetsinghd@gmail.com
|
|
102
103
|
executables: []
|
|
103
104
|
extensions: []
|
|
104
105
|
extra_rdoc_files: []
|
|
@@ -122,13 +123,16 @@ files:
|
|
|
122
123
|
- lib/trainers/trainer_utils.rb
|
|
123
124
|
- lib/trainers/training_arguments.rb
|
|
124
125
|
- lib/trainers/version.rb
|
|
125
|
-
homepage: https://github.com/
|
|
126
|
+
homepage: https://github.com/Selectus2/trainers-rb
|
|
126
127
|
licenses:
|
|
127
128
|
- MIT
|
|
128
129
|
metadata:
|
|
129
|
-
homepage_uri: https://github.com/
|
|
130
|
-
source_code_uri: https://github.com/
|
|
131
|
-
changelog_uri: https://github.com/
|
|
130
|
+
homepage_uri: https://github.com/Selectus2/trainers-rb
|
|
131
|
+
source_code_uri: https://github.com/Selectus2/trainers-rb
|
|
132
|
+
changelog_uri: https://github.com/Selectus2/trainers-rb/blob/main/CHANGELOG.md
|
|
133
|
+
bug_tracker_uri: https://github.com/Selectus2/trainers-rb/issues
|
|
134
|
+
rubygems_mfa_required: 'true'
|
|
135
|
+
post_install_message:
|
|
132
136
|
rdoc_options: []
|
|
133
137
|
require_paths:
|
|
134
138
|
- lib
|
|
@@ -143,7 +147,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
143
147
|
- !ruby/object:Gem::Version
|
|
144
148
|
version: '0'
|
|
145
149
|
requirements: []
|
|
146
|
-
rubygems_version:
|
|
150
|
+
rubygems_version: 3.5.22
|
|
151
|
+
signing_key:
|
|
147
152
|
specification_version: 4
|
|
148
153
|
summary: Fine-tune transformer models in Ruby
|
|
149
154
|
test_files: []
|