trainers-rb 0.1.1 → 0.2.0
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 +58 -0
- data/README.md +71 -7
- data/lib/trainers/callbacks.rb +23 -7
- data/lib/trainers/errors.rb +15 -0
- data/lib/trainers/logging.rb +27 -0
- data/lib/trainers/lora/lora_linear.rb +25 -1
- data/lib/trainers/lora/lora_model.rb +8 -10
- data/lib/trainers/lora/lora_utils.rb +17 -13
- data/lib/trainers/optimization/optimizer.rb +1 -1
- data/lib/trainers/save_utils.rb +5 -6
- data/lib/trainers/trainer.rb +77 -44
- data/lib/trainers/training_arguments.rb +44 -2
- data/lib/trainers/version.rb +1 -1
- data/lib/trainers.rb +10 -0
- metadata +24 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8db3543c735b87abb79d2de2d8278f11c3999289f2c68929a6cc7c855568b6a1
|
|
4
|
+
data.tar.gz: ff4783fde9226bff71d59e00960997109a5426f407572ea41859657a120e907f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 492022d310c9dc8b59e823e550769bdabbbd6af100d503ca82421c4fc3cef3aaea041f667c21d96d223b19fd5441c5b85fbd515a44f4ccb0a6820b471adcd28c
|
|
7
|
+
data.tar.gz: 92678a59b82c31a055c3e28bfe40b60a6c830368c9707d00ccb2481c43213127e8210d7547d2eff32f75d636c48632b8ef44056e9c0692841cc983b983c5297b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
**This release changes training results.** Runs that use `gradient_accumulation_steps > 1` now follow the intended learning-rate schedule. All runs now apply `seed:`, so data order is different from 0.1.x and reproducible. Expect different (better) final models than 0.1.x with the same arguments.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **A step is now one optimizer update** (#12). `global_step`, `max_steps`, `logging_steps`, `eval_steps`, `save_steps`, `warmup_steps` and the scheduler all count optimizer updates, not batches. The scheduler is sized as `ceil(batches_per_epoch / gradient_accumulation_steps) * num_train_epochs`, so it now decays to the end. Previously, with accumulation 4, a linear schedule ended near 75% of the peak learning rate.
|
|
10
|
+
- The last, shorter accumulation window of an epoch now makes an optimizer update, with its loss averaged over the batches it holds. Previously its gradients leaked into the next epoch.
|
|
11
|
+
- `on_step_begin` fires at the start of each accumulation window and `on_step_end` after each optimizer update. Nothing changes with `gradient_accumulation_steps: 1`.
|
|
12
|
+
- **`seed:` is applied** (#13). `train` seeds Torch, Ruby's RNG and its own shuffling. `seed: nil` disables this. New `Trainers.set_seed(seed)` seeds model initialisation.
|
|
13
|
+
- **Progress goes to `Trainers.logger`** on `$stderr`, not stdout (#15). Set `Trainers.logger = nil` for a silent library. The default callback is now `Trainers::LoggingCallback`; `PrinterCallback` still prints to stdout if you add it to `callbacks:`.
|
|
14
|
+
- **Errors descend from `Trainers::Error`** (#16): `ConfigurationError`, `AdapterError` and `CheckpointError`. Code that rescued `RuntimeError` for these must rescue `Trainers::Error` instead.
|
|
15
|
+
- `SaveUtils.save_lora_adapters` and `LoraModel.save_adapters` raise `AdapterError` when the model has no adapters, instead of printing and returning `nil` (#16).
|
|
16
|
+
- **Requires Ruby 3.3 or newer.** Ruby 3.1 and 3.2 are end-of-life.
|
|
17
|
+
- New runtime dependency: `logger`, which Ruby 3.5+ no longer ships as a default gem.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `dataloader_drop_last: true` skips the final partial batch in training, evaluation and prediction (#14). `train` raises `ConfigurationError` if that leaves no batches.
|
|
22
|
+
- `label_names:` is honoured (#14). Every listed key is passed to the model, and the first is used for the built-in cross-entropy loss and `evaluate`.
|
|
23
|
+
- `LoraModel.apply` works on models whose layers sit in a `ModuleList`, which covers every HuggingFace transformer, for example DistilBERT's `transformer.layer.0.attention.q_lin`. It used to raise `Could not find module part '0'`, because `ModuleList` stores its children in a Hash keyed by `"0"`, not in an Array.
|
|
24
|
+
- When an epoch's last optimizer step was already logged, the end-of-epoch log no longer repeats it.
|
|
25
|
+
- Learning rates and other floats below 0.001 are logged in scientific notation (`2.000e-05`), not as `0.0000`.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `TrainingArguments` validates `seed` (nil or a non-negative Integer) and `label_names` (an Array of Strings or Symbols).
|
|
30
|
+
|
|
31
|
+
## 0.1.2 (2026-10-30)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- `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`.
|
|
36
|
+
- `Trainers::VERSION` now matches the gem version. The gemspec reads it from `lib/trainers/version.rb`. (#2)
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `TrainingArguments` validates its values at construction, and raises `ArgumentError` naming the argument, the value received and what was expected (#8):
|
|
41
|
+
- positive: `learning_rate`
|
|
42
|
+
- positive Integer: `num_train_epochs`, both batch sizes, `gradient_accumulation_steps`, `logging_steps`, `save_steps`
|
|
43
|
+
- non-negative: `weight_decay`, `max_grad_norm`; `warmup_steps` must also be an Integer
|
|
44
|
+
- `warmup_ratio` must be within 0..1, and `eval_steps` must be nil or a positive Integer
|
|
45
|
+
- `lr_scheduler_type` must be one of `:linear`, `:cosine`, `:constant`; `eval_strategy` and `save_strategy` one of `:no`, `:epoch`, `:steps`
|
|
46
|
+
|
|
47
|
+
Previously these were accepted and failed later, or were silently ignored. Defaults are unchanged.
|
|
48
|
+
|
|
49
|
+
### Internal
|
|
50
|
+
|
|
51
|
+
- 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)
|
|
52
|
+
- 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)
|
|
53
|
+
- Development dependency minitest bumped to ~> 6.0
|
|
54
|
+
- CONTRIBUTING.md with LibTorch setup, SECURITY.md, README badges and a known-limitations section (#11)
|
|
55
|
+
|
|
56
|
+
## 0.1.1 (2026-06-09)
|
|
57
|
+
|
|
58
|
+
- Fix gemspec homepage, source and changelog URLs to point to github.com/Selectus2/trainers-rb
|
|
59
|
+
- No code changes. The published gem reports `Trainers::VERSION` as "0.1.0"; the constant is corrected here
|
|
60
|
+
|
|
3
61
|
## 0.1.0 (2026-06-07)
|
|
4
62
|
|
|
5
63
|
- 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
|
|
@@ -128,7 +134,7 @@ Trainers::LoraModel.load_adapters(model, "./lora-adapters")
|
|
|
128
134
|
| `learning_rate` | `5e-5` | Peak learning rate for AdamW |
|
|
129
135
|
| `weight_decay` | `0.0` | Weight decay (applied to non-bias, non-norm params) |
|
|
130
136
|
| `max_grad_norm` | `1.0` | Max gradient norm for clipping |
|
|
131
|
-
| `gradient_accumulation_steps` | `1` | Accumulate gradients over N
|
|
137
|
+
| `gradient_accumulation_steps` | `1` | Accumulate gradients over N batches per optimizer step |
|
|
132
138
|
| `warmup_steps` | `0` | Linear warmup steps |
|
|
133
139
|
| `warmup_ratio` | `0.0` | Warmup as fraction of total steps (alternative to warmup_steps) |
|
|
134
140
|
| `lr_scheduler_type` | `:linear` | `:linear`, `:cosine`, or `:constant` |
|
|
@@ -136,10 +142,60 @@ Trainers::LoraModel.load_adapters(model, "./lora-adapters")
|
|
|
136
142
|
| `eval_steps` | `nil` | Evaluate every N steps (when `eval_strategy: :steps`) |
|
|
137
143
|
| `save_strategy` | `:epoch` | When to save: `:no`, `:epoch`, or `:steps` |
|
|
138
144
|
| `save_total_limit` | `nil` | Keep only the last N checkpoints |
|
|
145
|
+
| `save_steps` | `500` | Save a checkpoint every N steps (when `save_strategy: :steps`) |
|
|
139
146
|
| `logging_steps` | `500` | Log every N steps |
|
|
140
|
-
| `seed` | `42` |
|
|
147
|
+
| `seed` | `42` | Seeds Torch, Ruby's RNG and data shuffling at the start of `train`; `nil` disables seeding |
|
|
148
|
+
| `dataloader_drop_last` | `false` | Skip the final batch when it is smaller than the batch size |
|
|
149
|
+
| `label_names` | `["labels"]` | Batch keys that hold labels; the first is used for the built-in cross-entropy loss |
|
|
141
150
|
| `no_mps` | `false` | Force CPU even if MPS is available |
|
|
142
151
|
|
|
152
|
+
**A step is one optimizer update.** With `gradient_accumulation_steps: 4`, one step covers four batches, and `max_steps`, `global_step`, `logging_steps`, `eval_steps`, `save_steps`, `warmup_steps` and the learning-rate schedule all count optimizer updates. When an epoch's batch count is not a multiple of the accumulation steps, the last, shorter window still makes an update, with its loss averaged over the batches it contains, so no gradient carries into the next epoch.
|
|
153
|
+
|
|
154
|
+
Invalid values, such as a negative learning rate, a zero batch size or an unknown `lr_scheduler_type`, raise `ArgumentError` when `TrainingArguments` is created.
|
|
155
|
+
|
|
156
|
+
## Reproducibility
|
|
157
|
+
|
|
158
|
+
`Trainer#train` seeds Torch, Ruby's global RNG and its own shuffling from `args.seed`. The model's initial weights are created before training starts, so seed those yourself:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
Trainers.set_seed(42)
|
|
162
|
+
model, tokenizer = Trainers.from_pretrained("distilbert-base-uncased", num_labels: 2)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Two runs with the same seed, data and arguments produce identical loss curves on CPU. On Apple Silicon (MPS), some kernels are not deterministic, so expect small differences between runs.
|
|
166
|
+
|
|
167
|
+
## Logging
|
|
168
|
+
|
|
169
|
+
trainers-rb reports progress through `Trainers.logger`, which writes to `$stderr` at `INFO` by default. Nothing is written to stdout.
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Trainers.logger = Rails.logger # use your app's logger
|
|
173
|
+
Trainers.logger.level = Logger::WARN # only warnings and errors
|
|
174
|
+
Trainers.logger = nil # completely silent
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
To print progress to stdout instead, add `Trainers::PrinterCallback.new` to `callbacks:`.
|
|
178
|
+
|
|
179
|
+
## Errors
|
|
180
|
+
|
|
181
|
+
Every error trainers-rb raises descends from `Trainers::Error`:
|
|
182
|
+
|
|
183
|
+
| Error | Raised when |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `Trainers::ConfigurationError` | The model has no trainable parameters, returns no loss and no labels are present, or the training data yields no batches |
|
|
186
|
+
| `Trainers::AdapterError` | No module matches `target_modules`, an adapter file is missing, or `save_adapters` finds no adapters |
|
|
187
|
+
| `Trainers::CheckpointError` | Reserved for checkpoint save and load errors |
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
begin
|
|
191
|
+
trainer.train
|
|
192
|
+
rescue Trainers::Error => e
|
|
193
|
+
# anything trainers-rb raised
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Invalid `TrainingArguments` values raise `ArgumentError`.
|
|
198
|
+
|
|
143
199
|
## Callbacks
|
|
144
200
|
|
|
145
201
|
Built-in callbacks:
|
|
@@ -274,6 +330,14 @@ trainers-rb
|
|
|
274
330
|
|
|
275
331
|
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
332
|
|
|
333
|
+
## Known limitations
|
|
334
|
+
|
|
335
|
+
These are open correctness issues. They are tracked, and listed here so nobody discovers them the hard way:
|
|
336
|
+
|
|
337
|
+
- **A saved model cannot be loaded back.** No `config.json` is written. ([#17](https://github.com/Selectus2/trainers-rb/issues/17))
|
|
338
|
+
- **Checkpoints cannot resume training.** Optimizer, scheduler and trainer state are not saved. ([#18](https://github.com/Selectus2/trainers-rb/issues/18))
|
|
339
|
+
- **LoRA checkpoints save the full state dict** rather than just the adapters. ([#19](https://github.com/Selectus2/trainers-rb/issues/19))
|
|
340
|
+
|
|
277
341
|
## Roadmap
|
|
278
342
|
|
|
279
343
|
- [ ] More model architectures in transformers-rb (GPT-2, Llama for text generation)
|
|
@@ -286,7 +350,7 @@ trainers-rb adds the training layer that transformers-rb intentionally omits. Bo
|
|
|
286
350
|
|
|
287
351
|
## Contributing
|
|
288
352
|
|
|
289
|
-
Bug reports and pull requests are welcome
|
|
353
|
+
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
354
|
|
|
291
355
|
## License
|
|
292
356
|
|
data/lib/trainers/callbacks.rb
CHANGED
|
@@ -44,32 +44,48 @@ module Trainers
|
|
|
44
44
|
def on_save(args, state, control, **kwargs); end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
#
|
|
47
|
+
# Prints training progress to stdout. Opt in with callbacks: [PrinterCallback.new]
|
|
48
48
|
class PrinterCallback < TrainerCallback
|
|
49
49
|
def on_log(args, state, control, logs: nil, **kwargs)
|
|
50
50
|
return unless logs
|
|
51
51
|
output = logs.map { |k, v| "#{k}: #{format_value(v)}" }.join(" ")
|
|
52
|
-
|
|
52
|
+
emit "[step #{state.global_step}] #{output}"
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def on_train_begin(args, state, control, **kwargs)
|
|
56
|
-
|
|
56
|
+
emit "Starting training: #{state.num_train_epochs} epochs, #{state.max_steps} total steps"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def on_train_end(args, state, control, **kwargs)
|
|
60
|
-
|
|
60
|
+
emit "Training complete. Total steps: #{state.global_step}"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def on_evaluate(args, state, control, metrics: nil, **kwargs)
|
|
64
64
|
return unless metrics
|
|
65
65
|
output = metrics.map { |k, v| "#{k}: #{format_value(v)}" }.join(" ")
|
|
66
|
-
|
|
66
|
+
emit "[eval step #{state.global_step}] #{output}"
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
|
+
def emit(message)
|
|
72
|
+
puts message
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Small floats such as learning rates (2e-5) would print as 0.0000
|
|
71
76
|
def format_value(v)
|
|
72
|
-
v.is_a?(Float)
|
|
77
|
+
return v.to_s unless v.is_a?(Float)
|
|
78
|
+
|
|
79
|
+
v.nonzero? && v.abs < 1e-3 ? format("%.3e", v) : format("%.4f", v)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Default callback: reports the same progress through Trainers.logger
|
|
84
|
+
class LoggingCallback < PrinterCallback
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def emit(message)
|
|
88
|
+
Trainers.log(:info, message)
|
|
73
89
|
end
|
|
74
90
|
end
|
|
75
91
|
|
|
@@ -95,7 +111,7 @@ module Trainers
|
|
|
95
111
|
else
|
|
96
112
|
@wait_count += 1
|
|
97
113
|
if @wait_count >= @patience
|
|
98
|
-
|
|
114
|
+
Trainers.log(:info, "Early stopping triggered after #{@wait_count} evaluations without improvement")
|
|
99
115
|
control.should_training_stop = true
|
|
100
116
|
end
|
|
101
117
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trainers
|
|
4
|
+
# Base class for every error trainers-rb raises. Rescue this to catch them all.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# The model, data or arguments cannot be trained as configured
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# LoRA adapters are missing, cannot be applied, or cannot be loaded
|
|
11
|
+
class AdapterError < Error; end
|
|
12
|
+
|
|
13
|
+
# A checkpoint cannot be written or read
|
|
14
|
+
class CheckpointError < Error; end
|
|
15
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module Trainers
|
|
6
|
+
class << self
|
|
7
|
+
# Where trainers-rb reports progress. Defaults to $stderr at INFO.
|
|
8
|
+
# Set to any Logger-compatible object, or nil for a silent library.
|
|
9
|
+
attr_accessor :logger
|
|
10
|
+
|
|
11
|
+
def default_logger
|
|
12
|
+
Logger.new($stderr, level: Logger::INFO, progname: "trainers").tap do |logger|
|
|
13
|
+
logger.formatter = proc do |severity, _time, progname, message|
|
|
14
|
+
prefix = severity == "INFO" ? "" : "#{severity}: "
|
|
15
|
+
"[#{progname}] #{prefix}#{message}\n"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Internal: log through Trainers.logger when one is set
|
|
21
|
+
def log(level, message)
|
|
22
|
+
logger&.public_send(level, message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
self.logger = default_logger
|
|
27
|
+
end
|
|
@@ -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 }
|
|
@@ -20,7 +20,7 @@ module Trainers
|
|
|
20
20
|
targets = LoraUtils.find_target_modules(model, config.target_modules)
|
|
21
21
|
|
|
22
22
|
if targets.empty?
|
|
23
|
-
raise "No Linear modules found matching target_modules: #{config.target_modules.inspect}. " \
|
|
23
|
+
raise AdapterError, "No Linear modules found matching target_modules: #{config.target_modules.inspect}. " \
|
|
24
24
|
"Available modules: #{model.named_modules.map(&:first).join(', ')}"
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -50,22 +50,20 @@ module Trainers
|
|
|
50
50
|
end
|
|
51
51
|
# :none — biases stay frozen (default)
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Trainers.log(:info, "LoRA applied to #{targets.size} modules: #{targets.keys.join(', ')}")
|
|
54
54
|
LoraUtils.print_trainable_parameters(model)
|
|
55
55
|
|
|
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
|
-
|
|
66
|
+
Trainers.log(:info, "Merged #{lora_modules.size} LoRA adapters into base model")
|
|
69
67
|
model
|
|
70
68
|
end
|
|
71
69
|
|
|
@@ -30,24 +30,28 @@ module Trainers
|
|
|
30
30
|
|
|
31
31
|
# Navigate to the parent module
|
|
32
32
|
parts[0...-1].each do |part|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
parent = parent.instance_variable_get(:@modules)[part.to_i]
|
|
36
|
-
else
|
|
37
|
-
parent = parent.instance_variable_get(:"@#{part}")
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
raise "Could not find module part '#{part}' in #{target_name}" unless parent
|
|
33
|
+
parent = child_module(parent, part)
|
|
34
|
+
raise AdapterError, "Could not find module part '#{part}' in #{target_name}" unless parent
|
|
41
35
|
end
|
|
42
36
|
|
|
43
37
|
child_name = parts.last
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
registered = parent.instance_variable_get(:@modules)
|
|
39
|
+
if registered&.key?(child_name)
|
|
40
|
+
registered[child_name] = new_module
|
|
46
41
|
else
|
|
47
42
|
parent.instance_variable_set(:"@#{child_name}", new_module)
|
|
48
43
|
end
|
|
49
44
|
end
|
|
50
45
|
|
|
46
|
+
# ModuleList and add_module children live in @modules, a Hash keyed by
|
|
47
|
+
# name ("0", "1", ...); everything else is an instance variable.
|
|
48
|
+
def self.child_module(parent, name)
|
|
49
|
+
registered = parent.instance_variable_get(:@modules)
|
|
50
|
+
return registered[name] if registered&.key?(name)
|
|
51
|
+
|
|
52
|
+
parent.instance_variable_get(:"@#{name}") if name.match?(/\A[a-z_]\w*\z/i)
|
|
53
|
+
end
|
|
54
|
+
|
|
51
55
|
# Count total and trainable parameters
|
|
52
56
|
def self.print_trainable_parameters(model)
|
|
53
57
|
total = 0
|
|
@@ -59,9 +63,9 @@ module Trainers
|
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
pct = total > 0 ? (trainable.to_f / total * 100) : 0.0
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
Trainers.log(:info, "trainable params: #{format_number(trainable)} || " \
|
|
67
|
+
"all params: #{format_number(total)} || " \
|
|
68
|
+
"trainable%: #{format('%.4f', pct)}%")
|
|
65
69
|
|
|
66
70
|
{ total: total, trainable: trainable, percentage: pct }
|
|
67
71
|
end
|
|
@@ -35,7 +35,7 @@ module Trainers
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
if param_groups.empty?
|
|
38
|
-
raise "No trainable parameters found. Did you forget to unfreeze the model or apply LoRA?"
|
|
38
|
+
raise ConfigurationError, "No trainable parameters found. Did you forget to unfreeze the model or apply LoRA?"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
Torch::Optim::AdamW.new(
|
data/lib/trainers/save_utils.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Trainers
|
|
|
18
18
|
model_path = File.join(output_dir, "model.safetensors")
|
|
19
19
|
require "safetensors"
|
|
20
20
|
Safetensors::Torch.save_file(tensors, model_path)
|
|
21
|
-
|
|
21
|
+
Trainers.log(:info, "Model saved to #{model_path}")
|
|
22
22
|
|
|
23
23
|
# Save tokenizer
|
|
24
24
|
if tokenizer
|
|
@@ -49,21 +49,20 @@ module Trainers
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
if lora_state.empty?
|
|
52
|
-
|
|
53
|
-
return
|
|
52
|
+
raise AdapterError, "No LoRA adapters found in model. Apply them with LoraModel.apply before saving."
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
adapter_path = File.join(output_dir, "adapter_model.safetensors")
|
|
57
56
|
require "safetensors"
|
|
58
57
|
Safetensors::Torch.save_file(lora_state, adapter_path)
|
|
59
|
-
|
|
58
|
+
Trainers.log(:info, "LoRA adapters saved to #{adapter_path} (#{lora_state.size} tensors)")
|
|
60
59
|
|
|
61
60
|
output_dir
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
def self.load_lora_adapters(model, input_dir)
|
|
65
64
|
adapter_path = File.join(input_dir, "adapter_model.safetensors")
|
|
66
|
-
raise "Adapter file not found: #{adapter_path}" unless File.exist?(adapter_path)
|
|
65
|
+
raise AdapterError, "Adapter file not found: #{adapter_path}" unless File.exist?(adapter_path)
|
|
67
66
|
|
|
68
67
|
require "safetensors"
|
|
69
68
|
lora_state = Safetensors::Torch.load_file(adapter_path)
|
|
@@ -78,7 +77,7 @@ module Trainers
|
|
|
78
77
|
end
|
|
79
78
|
end
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
Trainers.log(:info, "LoRA adapters loaded from #{adapter_path}")
|
|
82
81
|
end
|
|
83
82
|
end
|
|
84
83
|
end
|
data/lib/trainers/trainer.rb
CHANGED
|
@@ -25,25 +25,35 @@ module Trainers
|
|
|
25
25
|
@state = TrainerState.new
|
|
26
26
|
@control = TrainerControl.new
|
|
27
27
|
|
|
28
|
-
all_callbacks = [
|
|
28
|
+
all_callbacks = [LoggingCallback.new] + callbacks
|
|
29
29
|
@callback_handler = CallbackHandler.new(all_callbacks)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def train
|
|
33
33
|
device = @args.resolved_device
|
|
34
|
+
apply_seed
|
|
34
35
|
@model.to(device)
|
|
35
36
|
@model.train
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
batch_size = @args.per_device_train_batch_size
|
|
39
|
+
accum = @args.gradient_accumulation_steps
|
|
40
|
+
batches_per_epoch = num_batches(@train_dataset, batch_size)
|
|
41
|
+
if batches_per_epoch.zero?
|
|
42
|
+
raise ConfigurationError, "train_dataset yields no batches: #{@train_dataset.size} examples, " \
|
|
43
|
+
"batch size #{batch_size}, dataloader_drop_last #{@args.dataloader_drop_last}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# One step is one optimizer update. The last accumulation window of an
|
|
47
|
+
# epoch may be shorter than gradient_accumulation_steps; it still steps.
|
|
48
|
+
steps_per_epoch = (batches_per_epoch.to_f / accum).ceil
|
|
49
|
+
total_steps = steps_per_epoch * @args.num_train_epochs
|
|
41
50
|
|
|
42
|
-
@state.max_steps
|
|
51
|
+
@state.max_steps = total_steps
|
|
43
52
|
@state.num_train_epochs = @args.num_train_epochs
|
|
44
53
|
|
|
45
54
|
@optimizer = create_optimizer
|
|
46
55
|
@lr_scheduler = create_scheduler(total_steps)
|
|
56
|
+
@optimizer.zero_grad
|
|
47
57
|
|
|
48
58
|
@callback_handler.fire(:on_train_begin, @args, @state, @control)
|
|
49
59
|
|
|
@@ -52,38 +62,38 @@ module Trainers
|
|
|
52
62
|
@callback_handler.fire(:on_epoch_begin, @args, @state, @control)
|
|
53
63
|
@model.train
|
|
54
64
|
|
|
55
|
-
epoch_loss
|
|
56
|
-
|
|
65
|
+
epoch_loss = 0.0
|
|
66
|
+
epoch_batches = 0
|
|
67
|
+
|
|
68
|
+
each_batch(@train_dataset, batch_size, shuffle: true).with_index do |batch, index|
|
|
69
|
+
position = index % accum
|
|
70
|
+
window_start = index - position
|
|
71
|
+
window_size = [accum, batches_per_epoch - window_start].min
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
@callback_handler.fire(:on_step_begin, @args, @state, @control)
|
|
73
|
+
@callback_handler.fire(:on_step_begin, @args, @state, @control) if position.zero?
|
|
60
74
|
|
|
61
75
|
batch = move_to_device(batch, device)
|
|
62
76
|
loss = compute_loss(batch)
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else
|
|
67
|
-
loss
|
|
68
|
-
end
|
|
69
|
-
|
|
78
|
+
# Average the gradient over the micro-batches in this window
|
|
79
|
+
scaled_loss = window_size > 1 ? loss / window_size : loss
|
|
70
80
|
scaled_loss.backward
|
|
71
81
|
|
|
72
|
-
epoch_loss
|
|
73
|
-
|
|
74
|
-
@state.global_step += 1
|
|
82
|
+
epoch_loss += loss.item
|
|
83
|
+
epoch_batches += 1
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
next unless position == window_size - 1
|
|
86
|
+
|
|
87
|
+
clip_grad_norm!(@model.parameters, @args.max_grad_norm)
|
|
88
|
+
@optimizer.step
|
|
89
|
+
@lr_scheduler.step
|
|
90
|
+
@optimizer.zero_grad
|
|
91
|
+
@state.global_step += 1
|
|
82
92
|
|
|
83
93
|
# Logging
|
|
84
94
|
if should_log?
|
|
85
95
|
logs = {
|
|
86
|
-
loss: epoch_loss /
|
|
96
|
+
loss: epoch_loss / epoch_batches,
|
|
87
97
|
learning_rate: current_lr,
|
|
88
98
|
epoch: @state.epoch
|
|
89
99
|
}
|
|
@@ -109,11 +119,13 @@ module Trainers
|
|
|
109
119
|
break if @control.should_training_stop || @control.should_epoch_stop
|
|
110
120
|
end
|
|
111
121
|
|
|
112
|
-
# Epoch-level logging
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
# Epoch-level logging, unless the last step of the epoch already logged
|
|
123
|
+
unless @state.log_history.last&.fetch(:step, nil) == @state.global_step
|
|
124
|
+
epoch_avg_loss = epoch_batches > 0 ? epoch_loss / epoch_batches : 0.0
|
|
125
|
+
logs = { loss: epoch_avg_loss, learning_rate: current_lr, epoch: @state.epoch }
|
|
126
|
+
@state.log_history << logs.merge(step: @state.global_step)
|
|
127
|
+
@callback_handler.fire(:on_log, @args, @state, @control, logs: logs)
|
|
128
|
+
end
|
|
117
129
|
|
|
118
130
|
# Epoch-based evaluation
|
|
119
131
|
if @args.eval_strategy == :epoch && @eval_dataset
|
|
@@ -151,7 +163,7 @@ module Trainers
|
|
|
151
163
|
Torch.no_grad do
|
|
152
164
|
each_batch(dataset, @args.per_device_eval_batch_size) do |batch|
|
|
153
165
|
batch = move_to_device(batch, device)
|
|
154
|
-
labels = batch
|
|
166
|
+
labels = pop_labels(batch).values.first
|
|
155
167
|
|
|
156
168
|
output = forward(batch)
|
|
157
169
|
|
|
@@ -209,11 +221,12 @@ module Trainers
|
|
|
209
221
|
private
|
|
210
222
|
|
|
211
223
|
def compute_loss(batch)
|
|
212
|
-
|
|
224
|
+
label_values = pop_labels(batch)
|
|
225
|
+
labels = label_values.values.first
|
|
213
226
|
|
|
214
227
|
# Try passing labels to the model (some models compute loss internally)
|
|
215
228
|
output = begin
|
|
216
|
-
forward(
|
|
229
|
+
forward(batch.merge(label_values))
|
|
217
230
|
rescue => e
|
|
218
231
|
# If the model doesn't support labels kwarg (e.g. transformers-rb Todo),
|
|
219
232
|
# fall back to forward without labels + external loss
|
|
@@ -225,7 +238,7 @@ module Trainers
|
|
|
225
238
|
end
|
|
226
239
|
|
|
227
240
|
# Restore labels to batch for downstream use
|
|
228
|
-
batch
|
|
241
|
+
batch.merge!(label_values)
|
|
229
242
|
|
|
230
243
|
if output.respond_to?(:loss) && output.loss
|
|
231
244
|
output.loss
|
|
@@ -233,7 +246,7 @@ module Trainers
|
|
|
233
246
|
logits = output.respond_to?(:logits) ? output.logits : output
|
|
234
247
|
Torch::NN::F.cross_entropy(logits, labels)
|
|
235
248
|
else
|
|
236
|
-
raise "Model did not return a loss and no labels found in batch. " \
|
|
249
|
+
raise ConfigurationError, "Model did not return a loss and no labels found in batch. " \
|
|
237
250
|
"Either pass labels in your dataset or use a model that computes loss."
|
|
238
251
|
end
|
|
239
252
|
end
|
|
@@ -268,20 +281,40 @@ module Trainers
|
|
|
268
281
|
)
|
|
269
282
|
end
|
|
270
283
|
|
|
284
|
+
# Removes every key named in args.label_names (symbol or string) from the
|
|
285
|
+
# batch and returns them as { name_symbol => tensor }, in label_names order
|
|
286
|
+
def pop_labels(batch)
|
|
287
|
+
@args.label_names.each_with_object({}) do |name, labels|
|
|
288
|
+
value = batch.key?(name.to_sym) ? batch.delete(name.to_sym) : batch.delete(name.to_s)
|
|
289
|
+
labels[name.to_sym] = value unless value.nil?
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def num_batches(dataset, batch_size)
|
|
294
|
+
full, rest = dataset.size.divmod(batch_size)
|
|
295
|
+
rest.positive? && !@args.dataloader_drop_last ? full + 1 : full
|
|
296
|
+
end
|
|
297
|
+
|
|
271
298
|
def each_batch(dataset, batch_size, shuffle: false)
|
|
272
|
-
|
|
273
|
-
indices.shuffle! if shuffle
|
|
299
|
+
return enum_for(:each_batch, dataset, batch_size, shuffle: shuffle) unless block_given?
|
|
274
300
|
|
|
275
|
-
(0...dataset.size).
|
|
276
|
-
|
|
277
|
-
next if batch_indices.nil? || batch_indices.empty?
|
|
301
|
+
indices = (0...dataset.size).to_a
|
|
302
|
+
indices.shuffle!(random: @rng || Random.new) if shuffle
|
|
278
303
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
304
|
+
num_batches(dataset, batch_size).times do |i|
|
|
305
|
+
batch_indices = indices[i * batch_size, batch_size]
|
|
306
|
+
features = batch_indices.map { |j| dataset[j] }
|
|
307
|
+
yield @data_collator.call(features)
|
|
282
308
|
end
|
|
283
309
|
end
|
|
284
310
|
|
|
311
|
+
# Seeds Torch, Kernel#rand and the shuffle RNG from args.seed (nil skips)
|
|
312
|
+
def apply_seed
|
|
313
|
+
seed = @args.seed
|
|
314
|
+
@rng = seed.nil? ? Random.new : Random.new(seed)
|
|
315
|
+
Trainers.set_seed(seed) unless seed.nil?
|
|
316
|
+
end
|
|
317
|
+
|
|
285
318
|
def move_to_device(batch, device)
|
|
286
319
|
batch.each_with_object({}) do |(key, value), result|
|
|
287
320
|
result[key] = if value.is_a?(Torch::Tensor)
|
|
@@ -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,37 @@ 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
|
+
check(:seed, "nil or a non-negative Integer") { |v| v.nil? || (v.is_a?(Integer) && !v.negative?) }
|
|
92
|
+
check(:label_names, "an Array of Strings or Symbols") { |v| v.is_a?(Array) && v.all? { |n| n.is_a?(String) || n.is_a?(Symbol) } }
|
|
93
|
+
|
|
94
|
+
check(:lr_scheduler_type, "one of #{SCHEDULER_TYPES.map(&:inspect).join(', ')}") { |v| SCHEDULER_TYPES.include?(v) }
|
|
95
|
+
check(:eval_strategy, "one of #{STRATEGIES.map(&:inspect).join(', ')}") { |v| STRATEGIES.include?(v) }
|
|
96
|
+
check(:save_strategy, "one of #{STRATEGIES.map(&:inspect).join(', ')}") { |v| STRATEGIES.include?(v) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def check(key, expected)
|
|
100
|
+
value = send(key)
|
|
101
|
+
return if yield(value)
|
|
102
|
+
|
|
103
|
+
raise ArgumentError, "#{key} must be #{expected}, got #{value.inspect}"
|
|
104
|
+
end
|
|
63
105
|
end
|
|
64
106
|
end
|
data/lib/trainers/version.rb
CHANGED
data/lib/trainers.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "json"
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
|
|
7
7
|
require_relative "trainers/version"
|
|
8
|
+
require_relative "trainers/errors"
|
|
9
|
+
require_relative "trainers/logging"
|
|
8
10
|
require_relative "trainers/trainer_utils"
|
|
9
11
|
require_relative "trainers/training_arguments"
|
|
10
12
|
require_relative "trainers/data/dataset"
|
|
@@ -20,6 +22,14 @@ require_relative "trainers/lora/lora_model"
|
|
|
20
22
|
require_relative "trainers/trainer"
|
|
21
23
|
|
|
22
24
|
module Trainers
|
|
25
|
+
# Seeds Torch and Ruby's global RNG. Call before building a model to make
|
|
26
|
+
# its initial weights reproducible; Trainer#train also calls it with args.seed.
|
|
27
|
+
def self.set_seed(seed)
|
|
28
|
+
Torch.manual_seed(seed)
|
|
29
|
+
srand(seed)
|
|
30
|
+
seed
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
# Convenience method: load model + tokenizer and prepare for training
|
|
24
34
|
def self.from_pretrained(model_name, task: :sequence_classification, num_labels: 2)
|
|
25
35
|
require "transformers-rb"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trainers-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vishwajeetsingh Desurkar
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: 0.5.3
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: logger
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.5'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.5'
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
83
|
name: rake
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -85,20 +99,20 @@ dependencies:
|
|
|
85
99
|
requirements:
|
|
86
100
|
- - "~>"
|
|
87
101
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '
|
|
102
|
+
version: '6.0'
|
|
89
103
|
type: :development
|
|
90
104
|
prerelease: false
|
|
91
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
106
|
requirements:
|
|
93
107
|
- - "~>"
|
|
94
108
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '
|
|
109
|
+
version: '6.0'
|
|
96
110
|
description: Training loop, LoRA, and optimization utilities for fine-tuning HuggingFace
|
|
97
111
|
transformer models using torch-rb and transformers-rb. Supports full fine-tuning,
|
|
98
112
|
LoRA adapters, learning rate scheduling, callbacks, and model serialization via
|
|
99
113
|
safetensors.
|
|
100
114
|
email:
|
|
101
|
-
-
|
|
115
|
+
- vishwajeetsinghd@gmail.com
|
|
102
116
|
executables: []
|
|
103
117
|
extensions: []
|
|
104
118
|
extra_rdoc_files: []
|
|
@@ -111,6 +125,8 @@ files:
|
|
|
111
125
|
- lib/trainers/callbacks.rb
|
|
112
126
|
- lib/trainers/data/data_collator.rb
|
|
113
127
|
- lib/trainers/data/dataset.rb
|
|
128
|
+
- lib/trainers/errors.rb
|
|
129
|
+
- lib/trainers/logging.rb
|
|
114
130
|
- lib/trainers/lora/lora_config.rb
|
|
115
131
|
- lib/trainers/lora/lora_linear.rb
|
|
116
132
|
- lib/trainers/lora/lora_model.rb
|
|
@@ -129,6 +145,8 @@ metadata:
|
|
|
129
145
|
homepage_uri: https://github.com/Selectus2/trainers-rb
|
|
130
146
|
source_code_uri: https://github.com/Selectus2/trainers-rb
|
|
131
147
|
changelog_uri: https://github.com/Selectus2/trainers-rb/blob/main/CHANGELOG.md
|
|
148
|
+
bug_tracker_uri: https://github.com/Selectus2/trainers-rb/issues
|
|
149
|
+
rubygems_mfa_required: 'true'
|
|
132
150
|
rdoc_options: []
|
|
133
151
|
require_paths:
|
|
134
152
|
- lib
|
|
@@ -136,14 +154,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
136
154
|
requirements:
|
|
137
155
|
- - ">="
|
|
138
156
|
- !ruby/object:Gem::Version
|
|
139
|
-
version: 3.
|
|
157
|
+
version: 3.3.0
|
|
140
158
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
159
|
requirements:
|
|
142
160
|
- - ">="
|
|
143
161
|
- !ruby/object:Gem::Version
|
|
144
162
|
version: '0'
|
|
145
163
|
requirements: []
|
|
146
|
-
rubygems_version:
|
|
164
|
+
rubygems_version: 4.0.20
|
|
147
165
|
specification_version: 4
|
|
148
166
|
summary: Fine-tune transformer models in Ruby
|
|
149
167
|
test_files: []
|