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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +364 -0
- data/exe/rpeft +8 -0
- data/lib/rpeft/arguments.rb +29 -0
- data/lib/rpeft/backend/error_translation.rb +28 -0
- data/lib/rpeft/backend/python.rb +161 -0
- data/lib/rpeft/backend.rb +24 -0
- data/lib/rpeft/cli/command.rb +78 -0
- data/lib/rpeft/cli/doctor.rb +75 -0
- data/lib/rpeft/cli/generate.rb +69 -0
- data/lib/rpeft/cli/lora_merge.rb +40 -0
- data/lib/rpeft/cli/lora_train.rb +76 -0
- data/lib/rpeft/cli.rb +77 -0
- data/lib/rpeft/dataset.rb +151 -0
- data/lib/rpeft/device.rb +17 -0
- data/lib/rpeft/diagnosis.rb +44 -0
- data/lib/rpeft/directory.rb +69 -0
- data/lib/rpeft/errors.rb +26 -0
- data/lib/rpeft/generation.rb +63 -0
- data/lib/rpeft/lora/adapter.rb +171 -0
- data/lib/rpeft/lora.rb +204 -0
- data/lib/rpeft/messages.rb +30 -0
- data/lib/rpeft/model.rb +70 -0
- data/lib/rpeft/version.rb +5 -0
- data/lib/rpeft.rb +37 -0
- data/python/rpeft_backend/__init__.py +90 -0
- data/python/rpeft_backend/_chat.py +79 -0
- data/python/rpeft_backend/_data.py +59 -0
- data/python/rpeft_backend/_device.py +57 -0
- data/python/rpeft_backend/_diagnostics.py +77 -0
- data/python/rpeft_backend/_errors.py +24 -0
- data/python/rpeft_backend/_generate.py +73 -0
- data/python/rpeft_backend/_merge.py +37 -0
- data/python/rpeft_backend/_models.py +30 -0
- data/python/rpeft_backend/_training.py +170 -0
- data/python/rpeft_backend/_versions.py +39 -0
- metadata +113 -0
|
@@ -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: []
|