rpeft 0.1.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.
@@ -0,0 +1,170 @@
1
+ import gc
2
+ import os
3
+ import shutil
4
+
5
+ from . import _data, _device, _models
6
+ from ._errors import stage
7
+
8
+
9
+ def train_lora(config):
10
+ lora = config.get("lora", {})
11
+ resolved = _device.resolve_device(config.get("device"))
12
+ device = resolved["device"]
13
+ dtype = resolved["dtype"]
14
+ output_dir = config["output_dir"]
15
+
16
+ model = None
17
+ try:
18
+ tokenizer, model = _models.load_tokenizer_and_model(
19
+ config["base_model"],
20
+ dtype=dtype,
21
+ device=device,
22
+ trust_remote_code=config.get("trust_remote_code", False),
23
+ )
24
+ dataset = _data.load_tokenized_dataset(
25
+ config["dataset_path"],
26
+ tokenizer,
27
+ text_field=config.get("text_field", "text"),
28
+ max_length=config.get("max_length", 512),
29
+ dataset_format=config.get("dataset_format", "text"),
30
+ )
31
+ eval_dataset = None
32
+ if config.get("eval_dataset_path"):
33
+ eval_dataset = _data.load_tokenized_dataset(
34
+ config["eval_dataset_path"],
35
+ tokenizer,
36
+ text_field=config.get("text_field", "text"),
37
+ max_length=config.get("max_length", 512),
38
+ dataset_format=config.get("dataset_format", "text"),
39
+ )
40
+
41
+ with stage("training"):
42
+ model, target_modules, param_counts = _apply_lora(model, lora)
43
+ trainer = _build_trainer(model, tokenizer, dataset, eval_dataset, config, device, dtype)
44
+ resume = _resume_point(config)
45
+ metrics = trainer.train(resume_from_checkpoint=resume).metrics
46
+ steps = trainer.state.global_step
47
+ if eval_dataset is not None:
48
+ metrics = dict(metrics, **trainer.evaluate())
49
+
50
+ with stage("adapter_save"):
51
+ model.save_pretrained(output_dir)
52
+ # Checkpoints only serve resuming an interrupted run. Once the adapter is saved they would
53
+ # just travel with it wherever the adapter directory is saved or shared.
54
+ shutil.rmtree(_trainer_dir(output_dir), ignore_errors=True)
55
+
56
+ return _result(
57
+ config, device, dtype, target_modules, param_counts, metrics, steps, len(dataset)
58
+ )
59
+ finally:
60
+ model = None
61
+ gc.collect()
62
+ _device.empty_cache(device)
63
+
64
+
65
+ def _apply_lora(model, lora):
66
+ from peft import LoraConfig, TaskType, get_peft_model
67
+
68
+ lora_config = LoraConfig(
69
+ r=lora.get("r", 8),
70
+ lora_alpha=lora.get("alpha", 16),
71
+ lora_dropout=lora.get("dropout", 0.05),
72
+ # None lets PEFT pick per-architecture defaults (e.g. q_proj/v_proj).
73
+ target_modules=lora.get("target_modules"),
74
+ bias="none",
75
+ task_type=TaskType.CAUSAL_LM,
76
+ )
77
+ model = get_peft_model(model, lora_config)
78
+ model.print_trainable_parameters()
79
+
80
+ target_modules = model.peft_config["default"].target_modules
81
+ if isinstance(target_modules, (set, frozenset)):
82
+ target_modules = sorted(target_modules)
83
+ elif isinstance(target_modules, str):
84
+ target_modules = [target_modules]
85
+ else:
86
+ target_modules = list(target_modules)
87
+
88
+ return model, target_modules, model.get_nb_trainable_parameters()
89
+
90
+
91
+ def _build_trainer(model, tokenizer, dataset, eval_dataset, config, device, dtype):
92
+ from transformers import Trainer, TrainingArguments
93
+
94
+ args = TrainingArguments(
95
+ output_dir=_trainer_dir(config["output_dir"]),
96
+ num_train_epochs=config.get("epochs", 3),
97
+ per_device_train_batch_size=config.get("batch_size", 4),
98
+ learning_rate=config.get("learning_rate", 2e-4),
99
+ logging_steps=config.get("logging_steps", 5),
100
+ logging_strategy="steps",
101
+ eval_strategy="epoch" if eval_dataset is not None else "no",
102
+ save_strategy=config.get("save_strategy", "no"),
103
+ report_to=[],
104
+ seed=config.get("seed", 42),
105
+ bf16=(dtype == "bfloat16"),
106
+ fp16=False,
107
+ dataloader_pin_memory=(device == "cuda"),
108
+ use_cpu=(device == "cpu"),
109
+ optim="adamw_torch",
110
+ )
111
+ return Trainer(
112
+ model=model,
113
+ args=args,
114
+ train_dataset=dataset,
115
+ eval_dataset=eval_dataset,
116
+ data_collator=_collator(tokenizer),
117
+ )
118
+
119
+
120
+ def _result(config, device, dtype, target_modules, param_counts, metrics, steps, num_examples):
121
+ trainable_params, total_params = param_counts
122
+ effective = dict(config)
123
+ effective["device"] = device
124
+ effective["lora"] = dict(config.get("lora", {}))
125
+ effective["lora"]["target_modules"] = target_modules
126
+
127
+ return {
128
+ "adapter_path": config["output_dir"],
129
+ "base_model": config["base_model"],
130
+ "device": device,
131
+ "dtype": dtype,
132
+ "train_runtime_seconds": float(metrics.get("train_runtime", 0.0)),
133
+ "final_loss": float(metrics.get("train_loss", 0.0)),
134
+ "eval_loss": float(metrics["eval_loss"]) if "eval_loss" in metrics else None,
135
+ "num_examples": int(num_examples),
136
+ "steps": int(steps),
137
+ "trainable_params": int(trainable_params),
138
+ "total_params": int(total_params),
139
+ "effective_config": effective,
140
+ }
141
+
142
+
143
+ def _trainer_dir(output_dir):
144
+ return os.path.join(output_dir, ".trainer")
145
+
146
+
147
+ def _resume_point(config):
148
+ if not config.get("resume_from_checkpoint"):
149
+ return None
150
+
151
+ from transformers.trainer_utils import get_last_checkpoint
152
+
153
+ # A finished run removes its checkpoints, so there may be nothing to resume; train from the start then.
154
+ trainer_dir = _trainer_dir(config["output_dir"])
155
+ return get_last_checkpoint(trainer_dir) if os.path.isdir(trainer_dir) else None
156
+
157
+
158
+ def _collator(tokenizer):
159
+ # DataCollatorForLanguageModeling ignores every position holding the pad token. Tokenizers without
160
+ # a pad token of their own pad with EOS (see _models.load_tokenizer), and many that have one reuse
161
+ # EOS too, so the EOS that ends each example would never be learned. Only the padding the batch
162
+ # added is ignored here, found through the attention mask instead of the token id.
163
+ def collate(features):
164
+ batch = tokenizer.pad(features, return_tensors="pt")
165
+ labels = batch["input_ids"].clone()
166
+ labels[batch["attention_mask"] == 0] = -100
167
+ batch["labels"] = labels
168
+ return batch
169
+
170
+ return collate
@@ -0,0 +1,39 @@
1
+ import importlib.metadata
2
+ import re
3
+
4
+ from ._errors import BackendError
5
+
6
+ # transformers 4.56 is the first release that accepts the dtype argument the backend passes, and it loads
7
+ # checkpoints and .bin model files only with torch 2.6 or later.
8
+ MINIMUMS = {"torch": "2.6", "transformers": "4.56"}
9
+
10
+
11
+ def installed_version(name):
12
+ try:
13
+ return importlib.metadata.version(name)
14
+ except importlib.metadata.PackageNotFoundError:
15
+ return None
16
+
17
+
18
+ def is_outdated(name, version):
19
+ minimum = MINIMUMS.get(name)
20
+ if minimum is None or version is None:
21
+ return False
22
+ return _parse(version) < _parse(minimum)
23
+
24
+
25
+ def require_minimums():
26
+ outdated = [
27
+ "%s %s (needs %s or later)" % (name, version, MINIMUMS[name])
28
+ for name in MINIMUMS
29
+ for version in [installed_version(name)]
30
+ if is_outdated(name, version)
31
+ ]
32
+ if outdated:
33
+ raise BackendError("dependency", "outdated Python packages: %s" % ", ".join(outdated))
34
+
35
+
36
+ def _parse(version):
37
+ # Local suffixes such as "2.6.0+cpu" and pre-release tags are ignored; only the release numbers count.
38
+ match = re.match(r"\d+(?:\.\d+)*", version)
39
+ return tuple(int(part) for part in match.group(0).split(".")) if match else ()
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rpeft
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Koichi ITO
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fiddle
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: pycall
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.5'
40
+ description: A Ruby interface for LoRA fine-tuning of large language models. It drives
41
+ PyTorch, Hugging Face Transformers, and PEFT through PyCall, modeling the train,
42
+ adapter, and merge lifecycle with a Ruby API and CLI.
43
+ email:
44
+ - koic.ito@gmail.com
45
+ executables:
46
+ - rpeft
47
+ extensions: []
48
+ extra_rdoc_files:
49
+ - LICENSE.txt
50
+ - README.md
51
+ files:
52
+ - LICENSE.txt
53
+ - README.md
54
+ - exe/rpeft
55
+ - lib/rpeft.rb
56
+ - lib/rpeft/arguments.rb
57
+ - lib/rpeft/backend.rb
58
+ - lib/rpeft/backend/error_translation.rb
59
+ - lib/rpeft/backend/python.rb
60
+ - lib/rpeft/cli.rb
61
+ - lib/rpeft/cli/command.rb
62
+ - lib/rpeft/cli/doctor.rb
63
+ - lib/rpeft/cli/generate.rb
64
+ - lib/rpeft/cli/lora_merge.rb
65
+ - lib/rpeft/cli/lora_train.rb
66
+ - lib/rpeft/dataset.rb
67
+ - lib/rpeft/device.rb
68
+ - lib/rpeft/diagnosis.rb
69
+ - lib/rpeft/directory.rb
70
+ - lib/rpeft/errors.rb
71
+ - lib/rpeft/generation.rb
72
+ - lib/rpeft/lora.rb
73
+ - lib/rpeft/lora/adapter.rb
74
+ - lib/rpeft/messages.rb
75
+ - lib/rpeft/model.rb
76
+ - lib/rpeft/version.rb
77
+ - python/rpeft_backend/__init__.py
78
+ - python/rpeft_backend/_chat.py
79
+ - python/rpeft_backend/_data.py
80
+ - python/rpeft_backend/_device.py
81
+ - python/rpeft_backend/_diagnostics.py
82
+ - python/rpeft_backend/_errors.py
83
+ - python/rpeft_backend/_generate.py
84
+ - python/rpeft_backend/_merge.py
85
+ - python/rpeft_backend/_models.py
86
+ - python/rpeft_backend/_training.py
87
+ - python/rpeft_backend/_versions.py
88
+ homepage: https://github.com/koic/rpeft
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ allowed_push_host: https://rubygems.org
93
+ homepage_uri: https://github.com/koic/rpeft
94
+ changelog_uri: https://github.com/koic/rpeft/blob/master/CHANGELOG.md
95
+ rubygems_mfa_required: 'true'
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 3.0.0
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 4.0.16
111
+ specification_version: 4
112
+ summary: LoRA fine-tuning for LLMs from Ruby.
113
+ test_files: []