modelforge-finetuning 3.0.3__tar.gz → 3.0.5__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.
Files changed (109) hide show
  1. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/cli.py +2 -2
  2. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/evaluation/metrics.py +10 -18
  3. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/notebook_cli/__init__.py +2 -1
  4. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/notebook_cli/progress.py +11 -0
  5. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/notebook_cli/prompts.py +57 -0
  6. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/notebook_cli/wizard.py +145 -37
  7. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/services/training_service.py +10 -1
  8. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/strategies/qlora_strategy.py +9 -3
  9. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/strategies/sft_strategy.py +39 -2
  10. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/strategies/strategy_factory.py +34 -14
  11. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/chat_playground.py +1 -1
  12. {modelforge_finetuning-3.0.3/modelforge_finetuning.egg-info → modelforge_finetuning-3.0.5}/PKG-INFO +4 -1
  13. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5/modelforge_finetuning.egg-info}/PKG-INFO +4 -1
  14. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/modelforge_finetuning.egg-info/requires.txt +4 -0
  15. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/pyproject.toml +5 -1
  16. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/LICENSE +0 -0
  17. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/MANIFEST.in +0 -0
  18. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/asset-manifest.json +0 -0
  19. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/favicon.ico +0 -0
  20. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/favicon.jpg +0 -0
  21. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/index.html +0 -0
  22. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/logo192.png +0 -0
  23. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/logo512.png +0 -0
  24. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/manifest.json +0 -0
  25. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/robots.txt +0 -0
  26. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/css/main.5a3d6ea1.css +0 -0
  27. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/css/main.5a3d6ea1.css.map +0 -0
  28. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/css/main.9680427d.css +0 -0
  29. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/css/main.9680427d.css.map +0 -0
  30. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/453.6e4f956a.chunk.js +0 -0
  31. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/453.6e4f956a.chunk.js.map +0 -0
  32. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/453.ad6eb26e.chunk.js +0 -0
  33. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/453.ad6eb26e.chunk.js.map +0 -0
  34. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.2b0344e0.js +0 -0
  35. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.2b0344e0.js.LICENSE.txt +0 -0
  36. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.2b0344e0.js.map +0 -0
  37. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.9cc9257a.js +0 -0
  38. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.9cc9257a.js.LICENSE.txt +0 -0
  39. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.9cc9257a.js.map +0 -0
  40. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.9df15fbf.js +0 -0
  41. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.9df15fbf.js.LICENSE.txt +0 -0
  42. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/Frontend/build/static/js/main.9df15fbf.js.map +0 -0
  43. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/__init__.py +0 -0
  44. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/app.py +0 -0
  45. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/database/__init__.py +0 -0
  46. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/database/database_manager.py +0 -0
  47. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/database/models.py +0 -0
  48. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/dependencies.py +0 -0
  49. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/evaluation/__init__.py +0 -0
  50. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/evaluation/dataset_validator.py +0 -0
  51. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/exceptions.py +0 -0
  52. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/formatters/__init__.py +0 -0
  53. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/formatters/formatting_functions.py +0 -0
  54. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/globals/__init__.py +0 -0
  55. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/globals/globals.py +0 -0
  56. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/globals/globals_instance.py +0 -0
  57. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/logging_config.py +0 -0
  58. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/model_configs/high_end.json +0 -0
  59. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/model_configs/low_end.json +0 -0
  60. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/model_configs/mid_range.json +0 -0
  61. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/providers/__init__.py +0 -0
  62. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/providers/huggingface_provider.py +0 -0
  63. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/providers/provider_factory.py +0 -0
  64. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/providers/unsloth_provider.py +0 -0
  65. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/routers/__init__.py +0 -0
  66. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/routers/finetuning_router.py +0 -0
  67. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/routers/hub_management_router.py +0 -0
  68. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/routers/models_router.py +0 -0
  69. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/routers/playground_router.py +0 -0
  70. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/schemas/__init__.py +0 -0
  71. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/schemas/training_schemas.py +0 -0
  72. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/services/__init__.py +0 -0
  73. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/services/hardware_service.py +0 -0
  74. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/services/model_service.py +0 -0
  75. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/strategies/__init__.py +0 -0
  76. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/strategies/dpo_strategy.py +0 -0
  77. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/strategies/rlhf_strategy.py +0 -0
  78. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/low_qa_train_set.jsonl +0 -0
  79. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/low_summarization_train_set.jsonl +0 -0
  80. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/low_text_generation.jsonl +0 -0
  81. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/med_construction.jsonl +0 -0
  82. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/med_text_generation.jsonl +0 -0
  83. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/question_answer-train-data-sample.jsonl +0 -0
  84. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/test_dataset.json +0 -0
  85. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/test_financial_advisory_generation.jsonl +0 -0
  86. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/test_datasets/test_financial_advisory_generation_bak.jsonl +0 -0
  87. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/__init__.py +0 -0
  88. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/configs/hardware_thresholds.json +0 -0
  89. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/configs/tasks.json +0 -0
  90. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/device_utils.py +0 -0
  91. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/finetuning/__init__.py +0 -0
  92. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/finetuning/quantization.py +0 -0
  93. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/finetuning/settings_builder.py +0 -0
  94. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/hardware_detection/__init__.py +0 -0
  95. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/hardware_detection/config_manager.py +0 -0
  96. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/hardware_detection/hardware_detector.py +0 -0
  97. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/hardware_detection/hardware_detector_factory.py +0 -0
  98. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/hardware_detection/model_recommendation.py +0 -0
  99. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/hardware_detection/model_validator.py +0 -0
  100. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/settings_managers/DBManager.py +0 -0
  101. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/settings_managers/FileManager.py +0 -0
  102. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/ModelForge/utilities/settings_managers/__init__.py +0 -0
  103. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/README.md +0 -0
  104. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/modelforge_finetuning.egg-info/SOURCES.txt +0 -0
  105. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/modelforge_finetuning.egg-info/dependency_links.txt +0 -0
  106. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/modelforge_finetuning.egg-info/entry_points.txt +0 -0
  107. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/modelforge_finetuning.egg-info/top_level.txt +0 -0
  108. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/setup.cfg +0 -0
  109. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.5}/tests/test_device_utils.py +0 -0
@@ -65,14 +65,14 @@ def main():
65
65
  subcommand = sys.argv[1]
66
66
  if subcommand == "cli":
67
67
  try:
68
- from .notebook_cli.wizard import main as cli_main
68
+ from .notebook_cli.wizard import ModelForgeWizard
69
69
  except ImportError as e:
70
70
  logger.error(f"Failed to import CLI wizard or its dependencies: {e}")
71
71
  print("\nThe interactive CLI requires optional dependencies that are not installed.")
72
72
  print("Please install the CLI extras and try again, for example:")
73
73
  print(" pip install \"modelforge-finetuning[cli]\"")
74
74
  sys.exit(1)
75
- cli_main()
75
+ ModelForgeWizard(notebook=False).run()
76
76
  return
77
77
  else:
78
78
  print(f"Unknown subcommand: '{subcommand}'")
@@ -21,6 +21,10 @@ class MetricsCalculator:
21
21
  - Perplexity: exp(loss)
22
22
  - Loss: Cross-entropy loss
23
23
 
24
+ Note: When preprocess_logits_for_metrics is used, predictions contain
25
+ pre-computed scalar losses (one per eval batch) instead of full logit tensors.
26
+ This prevents RAM exhaustion during evaluation.
27
+
24
28
  Args:
25
29
  eval_pred: Evaluation prediction with predictions and labels
26
30
 
@@ -29,28 +33,16 @@ class MetricsCalculator:
29
33
  """
30
34
  logger.info("Computing causal LM metrics")
31
35
 
32
- import torch
33
- import torch.nn.functional as F
34
-
35
- predictions, labels = eval_pred.predictions, eval_pred.label_ids
36
-
37
- logits = torch.from_numpy(np.array(predictions)).float()
38
- lbl = torch.from_numpy(np.array(labels))
39
-
40
- shift_logits = logits[..., :-1, :].contiguous()
41
- shift_labels = lbl[..., 1:].contiguous()
36
+ predictions = eval_pred.predictions
42
37
 
43
- loss = F.cross_entropy(
44
- shift_logits.view(-1, shift_logits.size(-1)),
45
- shift_labels.view(-1).long(),
46
- ignore_index=-100,
47
- reduction="mean",
48
- )
49
- perplexity = float(torch.exp(loss))
38
+ # predictions is shape (num_eval_batches, 1) of pre-computed scalar losses
39
+ # (from _preprocess_logits_for_metrics in sft_strategy.py)
40
+ mean_loss = float(np.mean(predictions))
41
+ perplexity = float(np.exp(mean_loss))
50
42
 
51
43
  metrics = {
52
44
  "perplexity": perplexity,
53
- "eval_loss": float(loss),
45
+ "eval_loss": mean_loss,
54
46
  }
55
47
 
56
48
  logger.info(f"Causal LM metrics: {metrics}")
@@ -12,7 +12,8 @@ Usage from a notebook cell:
12
12
  run_cli()
13
13
 
14
14
  Usage from a terminal:
15
- modelforge-nb
15
+ modelforge-nb (notebook-safe input() prompts)
16
+ modelforge cli (interactive terminal with questionary)
16
17
  """
17
18
  from .wizard import ModelForgeWizard, main as run_cli
18
19
 
@@ -7,8 +7,13 @@ notebook environment, and falls back to tqdm for plain terminal use.
7
7
  from __future__ import annotations
8
8
 
9
9
 
10
+ _NOTEBOOK_OVERRIDE: bool | None = None
11
+
12
+
10
13
  def _is_notebook() -> bool:
11
14
  """Return True if running inside a Jupyter-compatible notebook kernel."""
15
+ if _NOTEBOOK_OVERRIDE is not None:
16
+ return _NOTEBOOK_OVERRIDE
12
17
  try:
13
18
  from IPython import get_ipython
14
19
  shell = get_ipython()
@@ -19,6 +24,12 @@ def _is_notebook() -> bool:
19
24
  return False
20
25
 
21
26
 
27
+ def set_notebook_mode(mode: bool) -> None:
28
+ """Override the auto-detected notebook mode."""
29
+ global _NOTEBOOK_OVERRIDE
30
+ _NOTEBOOK_OVERRIDE = mode
31
+
32
+
22
33
  class ProgressDisplay:
23
34
  """
24
35
  Unified progress display that works in notebooks and terminals.
@@ -33,6 +33,12 @@ def _is_notebook() -> bool:
33
33
  _IN_NOTEBOOK = _is_notebook()
34
34
 
35
35
 
36
+ def set_notebook_mode(mode: bool) -> None:
37
+ """Override the auto-detected notebook mode."""
38
+ global _IN_NOTEBOOK
39
+ _IN_NOTEBOOK = mode
40
+
41
+
36
42
  # ── Notebook-safe input() fallbacks ──────────────────────────────────────────
37
43
 
38
44
  def _nb_select(message: str, choices: list) -> str:
@@ -83,6 +89,14 @@ def _nb_confirm(message: str, default: bool = True) -> bool:
83
89
 
84
90
  # ── Choices ────────────────────────────────────────────────────────────────
85
91
 
92
+ _MAIN_MENU_CHOICES = [
93
+ questionary.Choice("Fine-tune a model", value="train"),
94
+ questionary.Choice("List past models", value="list"),
95
+ questionary.Choice("Test a fine-tuned model", value="test"),
96
+ questionary.Choice("Exit", value="exit"),
97
+ ]
98
+
99
+
86
100
  _TASK_CHOICES = [
87
101
  questionary.Choice("Text Generation (causal LM fine-tuning)", value="text-generation"),
88
102
  questionary.Choice("Summarization (seq-to-seq fine-tuning)", value="summarization"),
@@ -342,6 +356,49 @@ def prompt_hyperparams(defaults: dict) -> dict:
342
356
  return params
343
357
 
344
358
 
359
+ def prompt_main_menu() -> str:
360
+ """Ask the user what they want to do."""
361
+ if _IN_NOTEBOOK:
362
+ return _nb_select("What would you like to do?", _MAIN_MENU_CHOICES)
363
+ return questionary.select(
364
+ "What would you like to do?",
365
+ choices=_MAIN_MENU_CHOICES,
366
+ style=questionary.Style([("selected", "fg:cyan bold")]),
367
+ ).ask()
368
+
369
+
370
+ def prompt_select_model(models: list[dict]) -> dict | None:
371
+ """
372
+ Ask the user to select a model from a list.
373
+
374
+ Args:
375
+ models: List of model dictionaries from the database.
376
+
377
+ Returns:
378
+ Selected model dict, or None if cancelled.
379
+ """
380
+ choices = []
381
+ for m in models:
382
+ label = (
383
+ f"{m.get('name', 'unknown')} "
384
+ f"[{m.get('task', '?')} / {m.get('strategy', '?')}]"
385
+ )
386
+ choices.append(questionary.Choice(label, value=m["id"]))
387
+
388
+ if _IN_NOTEBOOK:
389
+ model_id = _nb_select("Select a model:", choices)
390
+ else:
391
+ model_id = questionary.select(
392
+ "Select a model:",
393
+ choices=choices,
394
+ style=questionary.Style([("selected", "fg:cyan bold")]),
395
+ ).ask()
396
+
397
+ if model_id is None:
398
+ return None
399
+ return next((m for m in models if m["id"] == model_id), None)
400
+
401
+
345
402
  def prompt_confirm_config(config: dict) -> bool:
346
403
  """
347
404
  Show a summary table of the final config and ask for confirmation.
@@ -30,57 +30,165 @@ class ModelForgeWizard:
30
30
  Uses existing Python service classes directly — no REST API calls.
31
31
  """
32
32
 
33
- def __init__(self):
33
+ def __init__(self, notebook: bool | None = None):
34
34
  from rich.console import Console
35
35
 
36
36
  self.console = Console()
37
37
 
38
+ if notebook is not None:
39
+ from . import prompts, progress
40
+ prompts.set_notebook_mode(notebook)
41
+ progress.set_notebook_mode(notebook)
42
+
38
43
  # ── Public ─────────────────────────────────────────────────────────────
39
44
 
40
45
  def run(self) -> None:
41
- """Execute the full wizard flow."""
42
- from pydantic import ValidationError
43
-
44
- from ..schemas.training_schemas import TrainingConfig
45
- from .prompts import prompt_confirm_config
46
+ """Show main menu in a loop, dispatching until the user exits."""
47
+ from .prompts import prompt_main_menu
46
48
 
47
49
  try:
48
50
  self._print_banner()
49
- hardware_info = self._detect_hardware()
50
- task = self._select_task()
51
- model_name = self._select_model(task, hardware_info)
52
- provider = self._select_provider()
53
- strategy = self._select_strategy()
54
- dataset_path = self._select_dataset(task, strategy)
55
- hyperparams = self._configure_hyperparams(hardware_info, strategy)
56
- config = self._build_config(
57
- task=task,
58
- model_name=model_name,
59
- provider=provider,
60
- strategy=strategy,
61
- dataset=dataset_path,
62
- compute_specs=hardware_info.get("compute_profile", "low_end"),
63
- hyperparams=hyperparams,
64
- )
65
- try:
66
- TrainingConfig(**config)
67
- except ValidationError as exc:
68
- errors = "; ".join(e["msg"] for e in exc.errors())
69
- self.console.print(
70
- f"\n[red bold]Invalid configuration:[/red bold] {errors}"
71
- )
72
- return
73
- confirmed = prompt_confirm_config(config)
74
- if not confirmed:
75
- self.console.print("\n[yellow]Training cancelled.[/yellow]")
76
- return
77
- self._run_training(config)
51
+ while True:
52
+ action = prompt_main_menu()
53
+ if action is None or action == "exit":
54
+ self.console.print("\n[dim]Goodbye![/dim]")
55
+ return
56
+ if action == "train":
57
+ self._run_train_wizard()
58
+ elif action == "list":
59
+ self._list_models()
60
+ elif action == "test":
61
+ self._test_model()
78
62
  except KeyboardInterrupt:
79
63
  self.console.print("\n\n[yellow]Interrupted — goodbye.[/yellow]")
80
64
  except Exception as exc:
81
65
  self.console.print(f"\n[red bold]Error:[/red bold] {exc}")
82
66
  raise
83
67
 
68
+ def _run_train_wizard(self) -> None:
69
+ """Execute the full training wizard flow."""
70
+ from pydantic import ValidationError
71
+
72
+ from ..schemas.training_schemas import TrainingConfig
73
+ from .prompts import prompt_confirm_config
74
+
75
+ hardware_info = self._detect_hardware()
76
+ task = self._select_task()
77
+ model_name = self._select_model(task, hardware_info)
78
+ provider = self._select_provider()
79
+ strategy = self._select_strategy()
80
+ dataset_path = self._select_dataset(task, strategy)
81
+ hyperparams = self._configure_hyperparams(hardware_info, strategy)
82
+ config = self._build_config(
83
+ task=task,
84
+ model_name=model_name,
85
+ provider=provider,
86
+ strategy=strategy,
87
+ dataset=dataset_path,
88
+ compute_specs=hardware_info.get("compute_profile", "low_end"),
89
+ hyperparams=hyperparams,
90
+ )
91
+ try:
92
+ TrainingConfig(**config)
93
+ except ValidationError as exc:
94
+ errors = "; ".join(e["msg"] for e in exc.errors())
95
+ self.console.print(
96
+ f"\n[red bold]Invalid configuration:[/red bold] {errors}"
97
+ )
98
+ return
99
+ confirmed = prompt_confirm_config(config)
100
+ if not confirmed:
101
+ self.console.print("\n[yellow]Training cancelled.[/yellow]")
102
+ return
103
+ self._run_training(config)
104
+
105
+ # ── List & Test ────────────────────────────────────────────────────────
106
+
107
+ def _list_models(self) -> None:
108
+ """Display a table of all fine-tuned models."""
109
+ from rich.table import Table
110
+
111
+ from ..dependencies import get_model_service
112
+
113
+ self.console.print("\n[bold]Past Fine-Tuned Models[/bold]\n")
114
+ model_service = get_model_service()
115
+ models = model_service.get_all_models()
116
+
117
+ if not models:
118
+ self.console.print(" [dim]No models found.[/dim]")
119
+ return
120
+
121
+ table = Table(show_header=True, header_style="bold cyan")
122
+ table.add_column("#", style="dim", width=3)
123
+ table.add_column("Name")
124
+ table.add_column("Base Model")
125
+ table.add_column("Task")
126
+ table.add_column("Strategy")
127
+ table.add_column("Provider")
128
+ table.add_column("Created")
129
+ table.add_column("Path", style="dim")
130
+
131
+ for i, m in enumerate(models, 1):
132
+ created = m.get("created_at", "")
133
+ if created:
134
+ created = created[:16].replace("T", " ")
135
+ table.add_row(
136
+ str(i),
137
+ m.get("name", "—"),
138
+ m.get("base_model", "—"),
139
+ m.get("task", "—"),
140
+ m.get("strategy", "—"),
141
+ m.get("provider", "—"),
142
+ created,
143
+ m.get("path", "—"),
144
+ )
145
+
146
+ self.console.print(table)
147
+
148
+ def _test_model(self) -> None:
149
+ """Let the user pick a fine-tuned model and launch the playground."""
150
+ import os
151
+
152
+ from rich.panel import Panel
153
+
154
+ from ..dependencies import get_model_service
155
+ from .prompts import prompt_select_model
156
+
157
+ self.console.print("\n[bold]Test a Fine-Tuned Model[/bold]\n")
158
+ model_service = get_model_service()
159
+ models = model_service.get_all_models()
160
+
161
+ if not models:
162
+ self.console.print(" [dim]No models found. Fine-tune a model first.[/dim]")
163
+ return
164
+
165
+ selected = prompt_select_model(models)
166
+ if selected is None:
167
+ return
168
+
169
+ model_path = selected.get("path", "")
170
+ if not model_path or not os.path.isdir(model_path):
171
+ self.console.print(
172
+ f"\n[red]Model path not found:[/red] {model_path}"
173
+ )
174
+ return
175
+
176
+ self.console.print(
177
+ Panel(
178
+ f"[bold]Model:[/bold] {selected.get('name', '—')}\n"
179
+ f"[bold]Path:[/bold] {model_path}\n\n"
180
+ "[dim]Type /bye to exit, /view_settings to inspect the model.[/dim]",
181
+ title="Playground",
182
+ border_style="cyan",
183
+ expand=False,
184
+ )
185
+ )
186
+
187
+ from ..utilities.chat_playground import PlaygroundModel
188
+
189
+ bot = PlaygroundModel(model_path=model_path)
190
+ bot.chat()
191
+
84
192
  # ── Steps ───────────────────────────────────────────────────────────────
85
193
 
86
194
  def _print_banner(self) -> None:
@@ -317,7 +425,7 @@ class ModelForgeWizard:
317
425
  # ── Entry point ──────────────────────────────────────────────────────────────
318
426
 
319
427
  def main() -> None:
320
- """Entry point for the ``modelforge-nb`` console script."""
428
+ """Entry point for the ``modelforge-nb`` console script (notebook mode)."""
321
429
  try:
322
430
  import questionary # noqa: F401
323
431
  import rich # noqa: F401
@@ -330,7 +438,7 @@ def main() -> None:
330
438
  ' pip install "modelforge-finetuning[cli]"\n'
331
439
  )
332
440
  sys.exit(1)
333
- ModelForgeWizard().run()
441
+ ModelForgeWizard(notebook=True).run()
334
442
 
335
443
 
336
444
  if __name__ == "__main__":
@@ -432,7 +432,16 @@ class TrainingService:
432
432
 
433
433
  # Train
434
434
  self.training_status["message"] = "Training in progress..."
435
- trainer.train()
435
+ provider_name = config.get("provider", "")
436
+ if provider_name == "unsloth":
437
+ try:
438
+ from unsloth import unsloth_train
439
+ unsloth_train(trainer)
440
+ except ImportError:
441
+ logger.warning("unsloth_train not available, falling back to trainer.train()")
442
+ trainer.train()
443
+ else:
444
+ trainer.train()
436
445
 
437
446
  # Save model
438
447
  self.training_status["message"] = "Saving model..."
@@ -22,6 +22,7 @@ except ImportError:
22
22
  from trl import SFTTrainer, SFTConfig
23
23
 
24
24
  from ..logging_config import logger
25
+ from .sft_strategy import _preprocess_logits_for_metrics
25
26
 
26
27
 
27
28
  class QLoRAStrategy:
@@ -211,7 +212,7 @@ class QLoRAStrategy:
211
212
  max_seq_length = 2048
212
213
 
213
214
  # QLoRA-optimized training arguments
214
- training_args = SFTConfig(
215
+ sft_kwargs = dict(
215
216
  output_dir=config.get("output_dir", "./checkpoints"),
216
217
  num_train_epochs=config.get("num_train_epochs", 1),
217
218
  per_device_train_batch_size=config.get("per_device_train_batch_size", 4),
@@ -227,7 +228,6 @@ class QLoRAStrategy:
227
228
  bf16=config.get("bf16", True), # Default True for CUDA; MPS overrides to False in training_service
228
229
  max_grad_norm=config.get("max_grad_norm", 0.3),
229
230
  max_steps=config.get("max_steps", -1),
230
- group_by_length=config.get("group_by_length", True),
231
231
  lr_scheduler_type=config.get("lr_scheduler_type", "cosine"),
232
232
  report_to="tensorboard",
233
233
  logging_dir=config.get("logging_dir", "./training_logs"),
@@ -245,12 +245,17 @@ class QLoRAStrategy:
245
245
  load_best_model_at_end=True if eval_dataset else False,
246
246
  metric_for_best_model="eval_loss" if eval_dataset else None,
247
247
  ddp_find_unused_parameters=False,
248
- use_cache=False,
249
248
  # Dataloader settings (MPS sets these to avoid multiprocessing issues)
250
249
  dataloader_num_workers=config.get("dataloader_num_workers", 0),
251
250
  dataloader_pin_memory=config.get("dataloader_pin_memory", True),
252
251
  )
253
252
 
253
+ # Unsloth's patched SFTConfig doesn't support group_by_length
254
+ if not config.get("_peft_already_applied"):
255
+ sft_kwargs["group_by_length"] = config.get("group_by_length", True)
256
+
257
+ training_args = SFTConfig(**sft_kwargs)
258
+
254
259
  # Build the PEFT config for SFTTrainer to apply LoRA
255
260
  # Skip if PEFT was already applied (e.g. by Unsloth provider)
256
261
  peft_config = None if config.get("_peft_already_applied") else self.get_peft_config(config)
@@ -265,6 +270,7 @@ class QLoRAStrategy:
265
270
  peft_config=peft_config,
266
271
  callbacks=callbacks or [],
267
272
  compute_metrics=compute_metrics,
273
+ preprocess_logits_for_metrics=_preprocess_logits_for_metrics,
268
274
  )
269
275
 
270
276
  logger.info("QLoRA SFTTrainer created successfully")
@@ -23,6 +23,37 @@ from trl import SFTTrainer, SFTConfig
23
23
  from ..logging_config import logger
24
24
 
25
25
 
26
+ def _preprocess_logits_for_metrics(logits, labels):
27
+ """
28
+ Reduce logit tensors to scalar loss per batch before CPU accumulation.
29
+
30
+ This prevents RAM exhaustion when the Trainer accumulates eval predictions.
31
+ Instead of storing full logit tensors (batch × seq_len × vocab_size),
32
+ we compute the loss on GPU and return only a scalar, reducing memory by
33
+ orders of magnitude (e.g., 19.7 GB/batch → bytes/batch).
34
+
35
+ Args:
36
+ logits: Model logits (batch, seq_len, vocab_size)
37
+ labels: Token labels (batch, seq_len)
38
+
39
+ Returns:
40
+ Scalar loss tensor for this batch, shape (1,)
41
+ """
42
+ import torch.nn.functional as F
43
+
44
+ shift_logits = logits[..., :-1, :].contiguous()
45
+ shift_labels = labels[..., 1:].contiguous()
46
+
47
+ loss = F.cross_entropy(
48
+ shift_logits.view(-1, shift_logits.size(-1)),
49
+ shift_labels.view(-1).long(),
50
+ ignore_index=-100,
51
+ reduction="mean",
52
+ )
53
+
54
+ return loss.unsqueeze(0)
55
+
56
+
26
57
  class SFTStrategy:
27
58
  """Supervised Fine-Tuning strategy using TRL's SFTTrainer."""
28
59
 
@@ -193,7 +224,7 @@ class SFTStrategy:
193
224
  max_seq_length = 2048
194
225
 
195
226
  # Create SFTConfig with training arguments
196
- training_args = SFTConfig(
227
+ sft_kwargs = dict(
197
228
  output_dir=config.get("output_dir", "./checkpoints"),
198
229
  num_train_epochs=config.get("num_train_epochs", 1),
199
230
  per_device_train_batch_size=config.get("per_device_train_batch_size", 1),
@@ -209,7 +240,6 @@ class SFTStrategy:
209
240
  bf16=config.get("bf16", False),
210
241
  max_grad_norm=config.get("max_grad_norm", 0.3),
211
242
  max_steps=config.get("max_steps", -1),
212
- group_by_length=config.get("group_by_length", True),
213
243
  lr_scheduler_type=config.get("lr_scheduler_type", "cosine"),
214
244
  report_to="tensorboard",
215
245
  logging_dir=config.get("logging_dir", "./training_logs"),
@@ -229,6 +259,12 @@ class SFTStrategy:
229
259
  dataloader_pin_memory=config.get("dataloader_pin_memory", True),
230
260
  )
231
261
 
262
+ # Unsloth's patched SFTConfig doesn't support group_by_length
263
+ if not config.get("_peft_already_applied"):
264
+ sft_kwargs["group_by_length"] = config.get("group_by_length", True)
265
+
266
+ training_args = SFTConfig(**sft_kwargs)
267
+
232
268
  # Build the PEFT config for SFTTrainer to apply LoRA
233
269
  # Skip if PEFT was already applied (e.g. by Unsloth provider)
234
270
  peft_config = None if config.get("_peft_already_applied") else self.get_peft_config(config)
@@ -243,6 +279,7 @@ class SFTStrategy:
243
279
  peft_config=peft_config,
244
280
  callbacks=callbacks or [],
245
281
  compute_metrics=compute_metrics,
282
+ preprocess_logits_for_metrics=_preprocess_logits_for_metrics,
246
283
  )
247
284
 
248
285
  logger.info("SFTTrainer created successfully")
@@ -1,22 +1,42 @@
1
1
  """
2
2
  Strategy factory for creating training strategy instances.
3
+
4
+ Strategy imports are deferred to avoid pulling in heavy dependencies (trl, wandb)
5
+ at module load time — which would break lightweight entry points like the notebook
6
+ CLI wizard that only need HardwareService.
3
7
  """
4
- from .sft_strategy import SFTStrategy
5
- from .rlhf_strategy import RLHFStrategy
6
- from .dpo_strategy import DPOStrategy
7
- from .qlora_strategy import QLoRAStrategy
8
8
  from ..exceptions import ConfigurationError
9
9
  from ..logging_config import logger
10
10
 
11
11
 
12
+ def _import_sft():
13
+ from .sft_strategy import SFTStrategy
14
+ return SFTStrategy
15
+
16
+
17
+ def _import_rlhf():
18
+ from .rlhf_strategy import RLHFStrategy
19
+ return RLHFStrategy
20
+
21
+
22
+ def _import_dpo():
23
+ from .dpo_strategy import DPOStrategy
24
+ return DPOStrategy
25
+
26
+
27
+ def _import_qlora():
28
+ from .qlora_strategy import QLoRAStrategy
29
+ return QLoRAStrategy
30
+
31
+
12
32
  class StrategyFactory:
13
33
  """Factory for creating training strategy instances."""
14
34
 
15
- _strategies = {
16
- "sft": SFTStrategy,
17
- "rlhf": RLHFStrategy,
18
- "dpo": DPOStrategy,
19
- "qlora": QLoRAStrategy,
35
+ _strategy_loaders = {
36
+ "sft": _import_sft,
37
+ "rlhf": _import_rlhf,
38
+ "dpo": _import_dpo,
39
+ "qlora": _import_qlora,
20
40
  }
21
41
 
22
42
  @classmethod
@@ -37,13 +57,13 @@ class StrategyFactory:
37
57
 
38
58
  strategy_name = strategy_name.lower()
39
59
 
40
- if strategy_name not in cls._strategies:
60
+ if strategy_name not in cls._strategy_loaders:
41
61
  raise ConfigurationError(
42
62
  f"Unknown training strategy: {strategy_name}. "
43
- f"Available strategies: {list(cls._strategies.keys())}"
63
+ f"Available strategies: {list(cls._strategy_loaders.keys())}"
44
64
  )
45
65
 
46
- strategy_class = cls._strategies[strategy_name]
66
+ strategy_class = cls._strategy_loaders[strategy_name]()
47
67
  return strategy_class()
48
68
 
49
69
  @classmethod
@@ -54,7 +74,7 @@ class StrategyFactory:
54
74
  Returns:
55
75
  List of strategy names
56
76
  """
57
- return list(cls._strategies.keys())
77
+ return list(cls._strategy_loaders.keys())
58
78
 
59
79
  @classmethod
60
80
  def register_strategy(cls, name: str, strategy_class):
@@ -66,4 +86,4 @@ class StrategyFactory:
66
86
  strategy_class: Strategy class
67
87
  """
68
88
  logger.info(f"Registering strategy: {name}")
69
- cls._strategies[name.lower()] = strategy_class
89
+ cls._strategy_loaders[name.lower()] = lambda _cls=strategy_class: _cls
@@ -78,7 +78,7 @@ class PlaygroundModel:
78
78
  input("\nPress Enter to exit...")
79
79
  exit(1)
80
80
  except KeyError:
81
- print(f"Pipeline task {self.modelforge_config['pipeline_task']} not found in definitions for the pipeline object of transformers.")
81
+ print(traceback.format_exc())
82
82
  input("\nPress Enter to exit...")
83
83
  exit(1)
84
84
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelforge-finetuning
3
- Version: 3.0.3
3
+ Version: 3.0.5
4
4
  Summary: ModelForge: A no-code toolkit for fine-tuning HuggingFace models
5
5
  Author-email: R3tr0 M1ll3r <r3tr0.m1ll3r@gmail.com>
6
6
  Project-URL: Documentation, https://modelforge-finetuning.readthedocs.io
@@ -36,6 +36,9 @@ Requires-Dist: questionary>=2.0.1; extra == "cli"
36
36
  Requires-Dist: rich>=13.0.0; extra == "cli"
37
37
  Requires-Dist: ipywidgets>=8.0.0; extra == "cli"
38
38
  Requires-Dist: ipython>=8.0.0; extra == "cli"
39
+ Provides-Extra: unsloth
40
+ Requires-Dist: unsloth; extra == "unsloth"
41
+ Requires-Dist: bitsandbytes==0.45.3; extra == "unsloth"
39
42
  Dynamic: license-file
40
43
 
41
44
  # ModelForge 🔧⚡
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelforge-finetuning
3
- Version: 3.0.3
3
+ Version: 3.0.5
4
4
  Summary: ModelForge: A no-code toolkit for fine-tuning HuggingFace models
5
5
  Author-email: R3tr0 M1ll3r <r3tr0.m1ll3r@gmail.com>
6
6
  Project-URL: Documentation, https://modelforge-finetuning.readthedocs.io
@@ -36,6 +36,9 @@ Requires-Dist: questionary>=2.0.1; extra == "cli"
36
36
  Requires-Dist: rich>=13.0.0; extra == "cli"
37
37
  Requires-Dist: ipywidgets>=8.0.0; extra == "cli"
38
38
  Requires-Dist: ipython>=8.0.0; extra == "cli"
39
+ Provides-Extra: unsloth
40
+ Requires-Dist: unsloth; extra == "unsloth"
41
+ Requires-Dist: bitsandbytes==0.45.3; extra == "unsloth"
39
42
  Dynamic: license-file
40
43
 
41
44
  # ModelForge 🔧⚡
@@ -27,3 +27,7 @@ ipython>=8.0.0
27
27
 
28
28
  [quantization]
29
29
  bitsandbytes==0.45.3
30
+
31
+ [unsloth]
32
+ unsloth
33
+ bitsandbytes==0.45.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "modelforge-finetuning"
3
- version = "3.0.3"
3
+ version = "3.0.5"
4
4
  description = "ModelForge: A no-code toolkit for fine-tuning HuggingFace models"
5
5
  authors = [
6
6
  {name = "R3tr0 M1ll3r", email = "r3tr0.m1ll3r@gmail.com"},
@@ -40,6 +40,10 @@ cli = [
40
40
  "ipywidgets>=8.0.0",
41
41
  "ipython>=8.0.0",
42
42
  ]
43
+ unsloth = [
44
+ "unsloth",
45
+ "bitsandbytes==0.45.3",
46
+ ]
43
47
 
44
48
  [tool.setuptools.packages.find]
45
49
  where = ["."]