lora-easy 2.1__tar.gz → 2.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lora-easy
3
- Version: 2.1
3
+ Version: 2.2
4
4
  Summary: A tiny OOP wrapper around PEFT for LoRA fine-tuning of causal LMs.
5
5
  Author: Freakwill
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lora-easy
3
- Version: 2.1
3
+ Version: 2.2
4
4
  Summary: A tiny OOP wrapper around PEFT for LoRA fine-tuning of causal LMs.
5
5
  Author: Freakwill
6
6
  License: MIT
@@ -87,20 +87,22 @@ class LoraModel:
87
87
 
88
88
  def __repr__(self) -> str:
89
89
  n = sum(p.numel() for p in self.model.parameters())
90
- return f"{self.name.title()}: LoraModel('{self.id_}', {n/1e9:.1f}B params, lora={'yes' if self._has_lora() else 'no'})"
90
+ return f"{self:t}: LoraModel('{self:i}', {n/1e9:.1f}B params, lora={'yes' if self._has_lora() else 'no'})"
91
91
 
92
92
  def __str__(self):
93
93
  return self.name.title()
94
94
 
95
- def __format__(self, spec=None):
96
- if spec is None:
95
+ def __format__(self, spec=''):
96
+ if spec == '':
97
97
  return self.name
98
98
  elif spec == 'l':
99
99
  return self.name.lower()
100
100
  elif spec == 't':
101
101
  return self.name
102
+ elif spec == 'i':
103
+ return self.id_
102
104
  else:
103
- raise ValueError('`spec` should be one of None | l | t')
105
+ raise ValueError('`spec` should be one of [empty] | l | t')
104
106
 
105
107
  def _has_lora(self) -> bool:
106
108
  return self.peft_model is not None
@@ -183,13 +185,12 @@ class LoraModel:
183
185
  Usage::
184
186
 
185
187
  with model.chat_session("./history.json", auto_save=True) as s:
186
- s.chat("Hello")
187
- s.chat("What do you think?")
188
+ s> "Hello"
189
+ s> "What do you think?"
188
190
  s.history # all turns so far
189
191
 
190
192
  When the history grows beyond ``max_tokens`` tokens it is automatically
191
193
  summarised by the model and replaced with a compact system message.
192
- ``system_prompt`` overrides ``self.system_prompt`` for the session.
193
194
  """
194
195
  from .session import _ChatSession
195
196
  return _ChatSession(self, save_path, auto_save, max_tokens,
@@ -197,7 +198,8 @@ class LoraModel:
197
198
 
198
199
  # -- Training -----------------------------------------------------------
199
200
 
200
- def train(self, conversations: list[dict], save_checkpoints: bool = False, **kwargs):
201
+ def train(self, conversations: list[dict], save_checkpoints: bool = False,
202
+ max_length: int = 256, **kwargs):
201
203
  """Fine-tune with LoRA on ShareGPT-format conversations.
202
204
 
203
205
  Pipeline: render each convo via chat template -> tokenize with pad+trunc ->
@@ -207,22 +209,31 @@ class LoraModel:
207
209
  Args:
208
210
  save_checkpoints: if set, save a checkpoint per epoch to
209
211
  ``./lora-output-{name}``. False (default) produces no files.
212
+ max_length: tokenizer truncation/padding length. Raise it when
213
+ conversations are long (multi-turn or long replies) —
214
+ truncation cuts from the END, which would clip the target
215
+ assistant reply. Default 256.
210
216
  """
211
217
  if not self.lora_enabled:
212
218
  self.enable_lora()
213
219
  self.model.config.use_cache = False
214
220
 
215
221
  texts = [self._format(self._render(c), add_gen=False) for c in conversations]
216
- tok = self.tokenizer(texts, truncation=True, padding="max_length", max_length=256)
222
+ tok = self.tokenizer(texts, truncation=True, padding="max_length",
223
+ max_length=max_length)
217
224
  dataset = [{"input_ids": inds, "attention_mask": ms,
218
225
  "labels": [-100 if m == 0 else i for i, m in zip(inds, ms)]}
219
226
  for inds, ms in zip(tok["input_ids"], tok["attention_mask"])]
220
227
 
221
- output_dir = f"./lora-output-{self.name.lower()}" if save_checkpoints else "./temp_output"
222
- default_args = {"epochs": 30, "lr": 3e-4, "per_device_train_batch_size": 4, "logging_steps": 5}
223
- kwargs = default_args | kwargs
228
+ # friendly defaults; translate to TrainingArguments names below
229
+ defaults = {"output_dir": f"./lora-output-{self:l}" if save_checkpoints else "./temp_output",
230
+ "epochs": 30, "lr": 3e-4,
231
+ "per_device_train_batch_size": 4, "logging_steps": 5}
232
+ kwargs = defaults | kwargs
224
233
  args = TrainingArguments(
225
- output_dir=output_dir, **kwargs,
234
+ num_train_epochs=kwargs.pop("epochs"),
235
+ learning_rate=kwargs.pop("lr"),
236
+ **kwargs,
226
237
  save_strategy="no" if not save_checkpoints else "epoch",
227
238
  report_to="none",
228
239
  )
@@ -272,10 +283,15 @@ class LoraModel:
272
283
  (Path(path) / "description.txt").write_text(self.description)
273
284
 
274
285
  def load(self, path: str | None = None):
275
- # load the adapter
276
- path = path or self.save_path
277
- self.peft_model = PeftModel.from_pretrained(self.base, path)
286
+ # load the adapter from a LOCAL directory
287
+ p = Path(path or self.save_path)
288
+ if not p.is_dir():
289
+ raise FileNotFoundError(
290
+ f"""Adapter directory not found: '{p}'.
291
+ If it lives elsewhere pass the path explicitly, e.g. m.load('/path/to/ivanka-lora').""")
292
+ self.peft_model = PeftModel.from_pretrained(self.base, str(p),
293
+ is_trainable=True)
278
294
  # restore description if it was saved alongside the adapter
279
- desc_file = Path(path) / "description.txt"
295
+ desc_file = p / "description.txt"
280
296
  if desc_file.exists():
281
297
  self._description = desc_file.read_text()
@@ -73,7 +73,7 @@ class _ChatSession:
73
73
  """
74
74
  self._maybe_summarize(prompt)
75
75
  self.history.append({"role": "user", "content": prompt})
76
- reply = self.model.chat(prompt, history=self.history[:-1],
76
+ reply = self.model.chat(prompt, history=self.history[:-1],
77
77
  system_prompt=self.system_prompt, **kwargs)
78
78
  self.history.append({"role": "assistant", "content": reply})
79
79
  return reply
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lora-easy"
7
- version = "2.1"
7
+ version = "2.2"
8
8
  description = "A tiny OOP wrapper around PEFT for LoRA fine-tuning of causal LMs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes