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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fd96717156bbd2f4d515f3b1a5aaea897f581b1888bb527124de305a6c2d536
4
- data.tar.gz: 4a4a764def0528487ee20adb5e1edce52d7a3d267f1c99e399f5f48a7a5d9e2b
3
+ metadata.gz: b2025ca407a70ba9d0a206306f48486eaae88854e767dd006aa9f37af752eaf1
4
+ data.tar.gz: 5e4a9e5a90d377791c5577399b88aab55e5d00468f4cae13710e0d140d482e5f
5
5
  SHA512:
6
- metadata.gz: 4a36d98cfc6a61d3fe7cad411a653e242b7f2a11f402e7add4ea2300bd9ee59601c8408b864a1094ba6486444b4b9197c524d89de9b50e8c74ee263b3d59ab0b
7
- data.tar.gz: da2ec1f4fd3d69e76f3d6a812aec49454ff9d99b99006e1b53f0086e4a60d1958d69b6a1b7447e033fcbf927c412cc3fea50954d0c3cd72e5ce49adf4140d95f
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
+ [![Gem Version](https://img.shields.io/gem/v/trainers-rb)](https://rubygems.org/gems/trainers-rb)
4
+ [![CI](https://github.com/Selectus2/trainers-rb/actions/workflows/ci.yml/badge.svg)](https://github.com/Selectus2/trainers-rb/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
6
+ [![Ruby](https://img.shields.io/badge/ruby-3.3%20%7C%203.4%20%7C%204.0-red.svg)](.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 requires LibTorch:
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.4.0.zip
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 on GitHub.
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
- count = 0
62
- model.named_modules.each do |_, mod|
63
- if mod.is_a?(LoraLinear)
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 #{count} LoRA adapters into base model"
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
- unknown = kwargs.keys - DEFAULTS.keys
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trainers
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
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.0
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: 1980-01-02 00:00:00.000000000 Z
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: '5.0'
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: '5.0'
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
- - selectus2@users.noreply.rubygems.org
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/trainers-rb/trainers-rb
126
+ homepage: https://github.com/Selectus2/trainers-rb
126
127
  licenses:
127
128
  - MIT
128
129
  metadata:
129
- homepage_uri: https://github.com/trainers-rb/trainers-rb
130
- source_code_uri: https://github.com/trainers-rb/trainers-rb
131
- changelog_uri: https://github.com/trainers-rb/trainers-rb/blob/main/CHANGELOG.md
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: 4.0.5
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: []