modelforge-finetuning 2.1.3__tar.gz → 2.2.0__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.
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/cli.py +24 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/evaluation/dataset_validator.py +4 -3
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/evaluation/metrics.py +22 -12
- modelforge_finetuning-2.2.0/ModelForge/notebook_cli/__init__.py +19 -0
- modelforge_finetuning-2.2.0/ModelForge/notebook_cli/progress.py +127 -0
- modelforge_finetuning-2.2.0/ModelForge/notebook_cli/prompts.py +273 -0
- modelforge_finetuning-2.2.0/ModelForge/notebook_cli/wizard.py +336 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/routers/playground_router.py +10 -3
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/schemas/training_schemas.py +15 -1
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/services/training_service.py +44 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/strategies/dpo_strategy.py +2 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/strategies/qlora_strategy.py +2 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/strategies/rlhf_strategy.py +2 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/strategies/sft_strategy.py +9 -2
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/chat_playground.py +13 -1
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/finetuning/quantization.py +18 -2
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/PKG-INFO +67 -54
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/README.md +57 -50
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/modelforge_finetuning.egg-info/PKG-INFO +67 -54
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/modelforge_finetuning.egg-info/SOURCES.txt +4 -7
- modelforge_finetuning-2.2.0/modelforge_finetuning.egg-info/entry_points.txt +3 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/modelforge_finetuning.egg-info/requires.txt +9 -1
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/pyproject.toml +15 -4
- modelforge_finetuning-2.1.3/ModelForge/cli_old.py +0 -54
- modelforge_finetuning-2.1.3/ModelForge/routers/finetuning_router_old.py +0 -563
- modelforge_finetuning-2.1.3/ModelForge/routers/models_router_old.py +0 -28
- modelforge_finetuning-2.1.3/ModelForge/utilities/finetuning/CausalLLMTuner.py +0 -222
- modelforge_finetuning-2.1.3/ModelForge/utilities/finetuning/Finetuner.py +0 -269
- modelforge_finetuning-2.1.3/ModelForge/utilities/finetuning/QuestionAnsweringTuner.py +0 -175
- modelforge_finetuning-2.1.3/ModelForge/utilities/finetuning/Seq2SeqLMTuner.py +0 -158
- modelforge_finetuning-2.1.3/modelforge_finetuning.egg-info/entry_points.txt +0 -2
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/LICENSE +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/MANIFEST.in +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/asset-manifest.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/favicon.ico +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/favicon.jpg +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/index.html +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/logo192.png +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/logo512.png +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/manifest.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/robots.txt +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/static/css/main.9680427d.css +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/static/css/main.9680427d.css.map +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/static/js/453.ad6eb26e.chunk.js +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/static/js/453.ad6eb26e.chunk.js.map +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/static/js/main.9df15fbf.js +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/static/js/main.9df15fbf.js.LICENSE.txt +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/Frontend/build/static/js/main.9df15fbf.js.map +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/app.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/database/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/database/database_manager.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/database/models.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/dependencies.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/evaluation/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/exceptions.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/formatters/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/formatters/formatting_functions.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/globals/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/globals/globals.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/globals/globals_instance.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/logging_config.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/model_configs/high_end.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/model_configs/low_end.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/model_configs/mid_range.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/providers/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/providers/huggingface_provider.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/providers/provider_factory.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/providers/unsloth_provider.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/routers/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/routers/finetuning_router.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/routers/hub_management_router.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/routers/models_router.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/schemas/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/services/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/services/hardware_service.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/services/model_service.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/strategies/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/strategies/strategy_factory.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/low_qa_train_set.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/low_summarization_train_set.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/low_text_generation.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/med_construction.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/med_text_generation.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/question_answer-train-data-sample.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/test_dataset.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/test_financial_advisory_generation.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/test_datasets/test_financial_advisory_generation_bak.jsonl +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/configs/hardware_thresholds.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/configs/tasks.json +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/finetuning/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/finetuning/settings_builder.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/hardware_detection/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/hardware_detection/config_manager.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/hardware_detection/hardware_detector.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/hardware_detection/hardware_detector_factory.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/hardware_detection/model_recommendation.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/hardware_detection/model_validator.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/settings_managers/DBManager.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/settings_managers/FileManager.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/utilities/settings_managers/__init__.py +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/modelforge_finetuning.egg-info/dependency_links.txt +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/modelforge_finetuning.egg-info/top_level.txt +0 -0
- {modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/setup.cfg +0 -0
|
@@ -50,7 +50,31 @@ def check_huggingface_login():
|
|
|
50
50
|
def main():
|
|
51
51
|
"""
|
|
52
52
|
Main entry point for ModelForge CLI.
|
|
53
|
+
|
|
54
|
+
Subcommands:
|
|
55
|
+
modelforge — start the web server
|
|
56
|
+
modelforge cli — launch the interactive CLI wizard
|
|
53
57
|
"""
|
|
58
|
+
if len(sys.argv) > 1:
|
|
59
|
+
subcommand = sys.argv[1]
|
|
60
|
+
if subcommand == "cli":
|
|
61
|
+
try:
|
|
62
|
+
from .notebook_cli.wizard import main as cli_main
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
logger.error(f"Failed to import CLI wizard or its dependencies: {e}")
|
|
65
|
+
print("\nThe interactive CLI requires optional dependencies that are not installed.")
|
|
66
|
+
print("Please install the CLI extras and try again, for example:")
|
|
67
|
+
print(" pip install \"modelforge-finetuning[cli]\"")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
cli_main()
|
|
70
|
+
return
|
|
71
|
+
else:
|
|
72
|
+
print(f"Unknown subcommand: '{subcommand}'")
|
|
73
|
+
print("Usage:")
|
|
74
|
+
print(" modelforge Start the web server")
|
|
75
|
+
print(" modelforge cli Launch the interactive CLI wizard")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
54
78
|
print("\n" + "=" * 80)
|
|
55
79
|
print(" __ __ _ _ _____ ")
|
|
56
80
|
print(" | \\/ | | | | | ___| ")
|
|
@@ -87,9 +87,10 @@ class DatasetValidator:
|
|
|
87
87
|
if len(required_fields) > 0:
|
|
88
88
|
for i, example in enumerate(dataset.select(range(min(10, len(dataset))))):
|
|
89
89
|
for field in required_fields:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
value = example.get(field)
|
|
91
|
+
if value is None or (isinstance(value, str) and not value.strip()):
|
|
92
|
+
raise DatasetValidationError(
|
|
93
|
+
f"Example {i} has empty required field '{field}'."
|
|
93
94
|
)
|
|
94
95
|
|
|
95
96
|
logger.info(
|
{modelforge_finetuning-2.1.3 → modelforge_finetuning-2.2.0}/ModelForge/evaluation/metrics.py
RENAMED
|
@@ -29,20 +29,27 @@ class MetricsCalculator:
|
|
|
29
29
|
"""
|
|
30
30
|
logger.info("Computing causal LM metrics")
|
|
31
31
|
|
|
32
|
+
import torch
|
|
33
|
+
import torch.nn.functional as F
|
|
34
|
+
|
|
32
35
|
predictions, labels = eval_pred.predictions, eval_pred.label_ids
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Fallback: estimate loss from predictions
|
|
40
|
-
loss = 0.0
|
|
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()
|
|
41
42
|
|
|
42
|
-
|
|
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))
|
|
43
50
|
|
|
44
51
|
metrics = {
|
|
45
|
-
"perplexity":
|
|
52
|
+
"perplexity": perplexity,
|
|
46
53
|
"eval_loss": float(loss),
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -69,6 +76,9 @@ class MetricsCalculator:
|
|
|
69
76
|
|
|
70
77
|
predictions, labels = eval_pred.predictions, eval_pred.label_ids
|
|
71
78
|
|
|
79
|
+
if predictions.ndim == 3:
|
|
80
|
+
predictions = np.argmax(predictions, axis=-1)
|
|
81
|
+
|
|
72
82
|
# Decode predictions and labels
|
|
73
83
|
if tokenizer is not None:
|
|
74
84
|
# Replace -100 in labels (used for padding)
|
|
@@ -88,9 +98,9 @@ class MetricsCalculator:
|
|
|
88
98
|
)
|
|
89
99
|
|
|
90
100
|
metrics = {
|
|
91
|
-
"rouge1": float(result["rouge1"]
|
|
92
|
-
"rouge2": float(result["rouge2"]
|
|
93
|
-
"rougeL": float(result["rougeL"]
|
|
101
|
+
"rouge1": float(result["rouge1"]),
|
|
102
|
+
"rouge2": float(result["rouge2"]),
|
|
103
|
+
"rougeL": float(result["rougeL"]),
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
logger.info(f"Seq2Seq metrics: {metrics}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ModelForge Notebook CLI — public API.
|
|
3
|
+
|
|
4
|
+
Provides an interactive wizard-style interface that calls the ModelForge
|
|
5
|
+
Python APIs directly, without going through the REST/web layer.
|
|
6
|
+
|
|
7
|
+
Install the required extras with:
|
|
8
|
+
pip install modelforge-finetuning[cli]
|
|
9
|
+
|
|
10
|
+
Usage from a notebook cell:
|
|
11
|
+
from ModelForge.notebook_cli import run_cli
|
|
12
|
+
run_cli()
|
|
13
|
+
|
|
14
|
+
Usage from a terminal:
|
|
15
|
+
modelforge-nb
|
|
16
|
+
"""
|
|
17
|
+
from .wizard import ModelForgeWizard, main as run_cli
|
|
18
|
+
|
|
19
|
+
__all__ = ["ModelForgeWizard", "run_cli"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notebook-aware progress display for ModelForge CLI.
|
|
3
|
+
|
|
4
|
+
Automatically uses ipywidgets when running inside a Jupyter/Colab/VSCode
|
|
5
|
+
notebook environment, and falls back to tqdm for plain terminal use.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_notebook() -> bool:
|
|
11
|
+
"""Return True if running inside a Jupyter-compatible notebook kernel."""
|
|
12
|
+
try:
|
|
13
|
+
from IPython import get_ipython
|
|
14
|
+
shell = get_ipython()
|
|
15
|
+
if shell is None:
|
|
16
|
+
return False
|
|
17
|
+
return shell.__class__.__name__ == "ZMQInteractiveShell"
|
|
18
|
+
except ImportError:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProgressDisplay:
|
|
23
|
+
"""
|
|
24
|
+
Unified progress display that works in notebooks and terminals.
|
|
25
|
+
|
|
26
|
+
Usage::
|
|
27
|
+
|
|
28
|
+
display = ProgressDisplay()
|
|
29
|
+
display.start("Loading model...")
|
|
30
|
+
display.update(50, "Halfway there")
|
|
31
|
+
display.update(100, "Done!")
|
|
32
|
+
display.close()
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._notebook = _is_notebook()
|
|
37
|
+
self._bar = None
|
|
38
|
+
self._label = None
|
|
39
|
+
self._tqdm_bar = None
|
|
40
|
+
self._last_value = 0
|
|
41
|
+
|
|
42
|
+
def start(self, message: str = "Starting...") -> None:
|
|
43
|
+
"""Initialise and display the progress bar."""
|
|
44
|
+
self._last_value = 0
|
|
45
|
+
if self._notebook:
|
|
46
|
+
self._start_widget(message)
|
|
47
|
+
else:
|
|
48
|
+
self._start_tqdm(message)
|
|
49
|
+
|
|
50
|
+
def _start_widget(self, message: str) -> None:
|
|
51
|
+
try:
|
|
52
|
+
import ipywidgets as widgets
|
|
53
|
+
from IPython.display import display
|
|
54
|
+
|
|
55
|
+
self._bar = widgets.IntProgress(
|
|
56
|
+
value=0,
|
|
57
|
+
min=0,
|
|
58
|
+
max=100,
|
|
59
|
+
description="",
|
|
60
|
+
bar_style="info",
|
|
61
|
+
layout=widgets.Layout(width="100%"),
|
|
62
|
+
)
|
|
63
|
+
self._label = widgets.Label(value=message)
|
|
64
|
+
box = widgets.VBox([self._label, self._bar])
|
|
65
|
+
display(box)
|
|
66
|
+
except ImportError:
|
|
67
|
+
# ipywidgets not available — fall through to tqdm
|
|
68
|
+
self._notebook = False
|
|
69
|
+
self._start_tqdm(message)
|
|
70
|
+
|
|
71
|
+
def _start_tqdm(self, message: str) -> None:
|
|
72
|
+
from tqdm import tqdm
|
|
73
|
+
|
|
74
|
+
self._tqdm_bar = tqdm(
|
|
75
|
+
total=100,
|
|
76
|
+
desc=message,
|
|
77
|
+
bar_format="{desc}: {percentage:3.0f}%|{bar}| [{elapsed}]",
|
|
78
|
+
dynamic_ncols=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def update(self, value: int, message: str = "") -> None:
|
|
82
|
+
"""
|
|
83
|
+
Update progress.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
value: Progress value 0-100.
|
|
87
|
+
message: Status message to display alongside the bar.
|
|
88
|
+
"""
|
|
89
|
+
value = max(0, min(100, value))
|
|
90
|
+
if self._notebook:
|
|
91
|
+
if self._bar is not None:
|
|
92
|
+
self._bar.value = value
|
|
93
|
+
if value >= 100:
|
|
94
|
+
self._bar.bar_style = "success"
|
|
95
|
+
if self._label is not None and message:
|
|
96
|
+
self._label.value = message
|
|
97
|
+
else:
|
|
98
|
+
if self._tqdm_bar is not None:
|
|
99
|
+
delta = value - self._last_value
|
|
100
|
+
if delta > 0:
|
|
101
|
+
self._tqdm_bar.update(delta)
|
|
102
|
+
if message:
|
|
103
|
+
self._tqdm_bar.set_description(message)
|
|
104
|
+
self._last_value = value
|
|
105
|
+
|
|
106
|
+
def close(self) -> None:
|
|
107
|
+
"""Close/finalize the progress display."""
|
|
108
|
+
if self._notebook:
|
|
109
|
+
if self._bar is not None:
|
|
110
|
+
self._bar.bar_style = "success"
|
|
111
|
+
else:
|
|
112
|
+
if self._tqdm_bar is not None:
|
|
113
|
+
self._tqdm_bar.close()
|
|
114
|
+
self._tqdm_bar = None
|
|
115
|
+
|
|
116
|
+
def error(self) -> None:
|
|
117
|
+
"""Mark the progress bar as errored."""
|
|
118
|
+
if self._notebook:
|
|
119
|
+
if self._bar is not None:
|
|
120
|
+
self._bar.bar_style = "danger"
|
|
121
|
+
if self._label is not None:
|
|
122
|
+
self._label.value = "Training failed."
|
|
123
|
+
else:
|
|
124
|
+
if self._tqdm_bar is not None:
|
|
125
|
+
self._tqdm_bar.set_description("Training failed")
|
|
126
|
+
self._tqdm_bar.close()
|
|
127
|
+
self._tqdm_bar = None
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive prompt steps for the ModelForge notebook CLI wizard.
|
|
3
|
+
|
|
4
|
+
Each function handles one wizard step and returns a typed value.
|
|
5
|
+
All prompts use questionary; display panels use rich.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import questionary
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..services.training_service import TrainingService
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
# ── Choices ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
_TASK_CHOICES = [
|
|
25
|
+
questionary.Choice("Text Generation (causal LM fine-tuning)", value="text-generation"),
|
|
26
|
+
questionary.Choice("Summarization (seq-to-seq fine-tuning)", value="summarization"),
|
|
27
|
+
questionary.Choice(
|
|
28
|
+
"Extractive QA (question-answering fine-tuning)",
|
|
29
|
+
value="extractive-question-answering",
|
|
30
|
+
),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
_PROVIDER_CHOICES = [
|
|
34
|
+
questionary.Choice("HuggingFace (default, broad compatibility)", value="huggingface"),
|
|
35
|
+
questionary.Choice("Unsloth (faster training, single-GPU optimised)", value="unsloth"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
_STRATEGY_CHOICES = [
|
|
39
|
+
questionary.Choice("SFT – Supervised Fine-Tuning (recommended for most tasks)", value="sft"),
|
|
40
|
+
questionary.Choice("QLoRA – Quantised LoRA (memory-efficient fine-tuning)", value="qlora"),
|
|
41
|
+
questionary.Choice("DPO – Direct Preference Optimisation", value="dpo"),
|
|
42
|
+
questionary.Choice("RLHF – Preference alignment (DPO-based, no reward model needed)", value="rlhf"),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
_TASK_FORMAT_HELP = {
|
|
46
|
+
"text-generation": 'JSON Lines with "input" and "output" fields.',
|
|
47
|
+
"summarization": 'JSON Lines with "document" and "summary" fields.',
|
|
48
|
+
"extractive-question-answering": 'JSON Lines with "context", "question", and "answers" fields.',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_STRATEGY_FORMAT_HELP = {
|
|
52
|
+
"dpo": 'JSON Lines with "prompt", "chosen", and "rejected" fields.',
|
|
53
|
+
"rlhf": 'JSON Lines with "prompt", "chosen", and "rejected" fields.',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Step functions ──────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def prompt_task() -> str:
|
|
60
|
+
"""Ask the user to select a fine-tuning task."""
|
|
61
|
+
return questionary.select(
|
|
62
|
+
"Select fine-tuning task:",
|
|
63
|
+
choices=_TASK_CHOICES,
|
|
64
|
+
style=questionary.Style([("selected", "fg:cyan bold")]),
|
|
65
|
+
).ask()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def prompt_model(recommended: str, alternatives: list[str]) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Ask the user to select or enter a model name.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
recommended: Hardware-recommended primary model.
|
|
74
|
+
alternatives: List of alternative model suggestions.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
HuggingFace model ID string.
|
|
78
|
+
"""
|
|
79
|
+
choices = [questionary.Choice(f"{recommended} (recommended for your hardware)", value=recommended)]
|
|
80
|
+
for alt in alternatives:
|
|
81
|
+
choices.append(questionary.Choice(alt, value=alt))
|
|
82
|
+
choices.append(questionary.Choice("Enter a custom model ID…", value="__custom__"))
|
|
83
|
+
|
|
84
|
+
selection = questionary.select(
|
|
85
|
+
"Select a base model:",
|
|
86
|
+
choices=choices,
|
|
87
|
+
style=questionary.Style([("selected", "fg:cyan bold")]),
|
|
88
|
+
).ask()
|
|
89
|
+
|
|
90
|
+
if selection == "__custom__":
|
|
91
|
+
selection = questionary.text(
|
|
92
|
+
"Enter HuggingFace model ID (e.g. meta-llama/Llama-3.2-1B):",
|
|
93
|
+
validate=lambda v: bool(v.strip()) or "Model ID cannot be empty",
|
|
94
|
+
).ask()
|
|
95
|
+
|
|
96
|
+
return selection
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def prompt_dataset(task: str, strategy: str, training_service: "TrainingService") -> str:
|
|
100
|
+
"""
|
|
101
|
+
Ask for a dataset path and validate it via TrainingService.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
task: Selected task type.
|
|
105
|
+
strategy: Selected strategy.
|
|
106
|
+
training_service: TrainingService instance for validation.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Validated dataset path string.
|
|
110
|
+
"""
|
|
111
|
+
fmt = _STRATEGY_FORMAT_HELP.get(strategy) or _TASK_FORMAT_HELP.get(task, "JSON Lines file.")
|
|
112
|
+
console.print(f"\n[dim]Expected format:[/dim] {fmt}")
|
|
113
|
+
|
|
114
|
+
while True:
|
|
115
|
+
path = questionary.path(
|
|
116
|
+
"Path to dataset file (.jsonl / .json):",
|
|
117
|
+
validate=lambda v: os.path.isfile(v) or "File not found — please enter a valid path",
|
|
118
|
+
).ask()
|
|
119
|
+
|
|
120
|
+
if path is None:
|
|
121
|
+
raise KeyboardInterrupt
|
|
122
|
+
|
|
123
|
+
console.print(" [dim]Validating dataset…[/dim]")
|
|
124
|
+
try:
|
|
125
|
+
info = training_service.validate_and_prepare_dataset(path, task, strategy)
|
|
126
|
+
console.print(
|
|
127
|
+
f" [green]✓[/green] Dataset valid — "
|
|
128
|
+
f"[bold]{info['num_examples']}[/bold] examples, "
|
|
129
|
+
f"fields: {info['fields']}"
|
|
130
|
+
)
|
|
131
|
+
return path
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
console.print(f" [red]✗[/red] Validation failed: {exc}")
|
|
134
|
+
retry = questionary.confirm("Try a different file?", default=True).ask()
|
|
135
|
+
if not retry:
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def prompt_provider() -> str:
|
|
140
|
+
"""Ask the user to choose a training provider."""
|
|
141
|
+
return questionary.select(
|
|
142
|
+
"Select training provider:",
|
|
143
|
+
choices=_PROVIDER_CHOICES,
|
|
144
|
+
style=questionary.Style([("selected", "fg:cyan bold")]),
|
|
145
|
+
).ask()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def prompt_strategy() -> str:
|
|
149
|
+
"""Ask the user to choose a training strategy."""
|
|
150
|
+
return questionary.select(
|
|
151
|
+
"Select training strategy:",
|
|
152
|
+
choices=_STRATEGY_CHOICES,
|
|
153
|
+
style=questionary.Style([("selected", "fg:cyan bold")]),
|
|
154
|
+
).ask()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def prompt_hyperparams(defaults: dict) -> dict:
|
|
158
|
+
"""
|
|
159
|
+
Interactively configure hyperparameters, showing defaults.
|
|
160
|
+
|
|
161
|
+
Users can press Enter to keep each default value.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
defaults: Dictionary of default hyperparameter values.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dictionary with final hyperparameter values.
|
|
168
|
+
"""
|
|
169
|
+
console.print(
|
|
170
|
+
Panel(
|
|
171
|
+
"[bold]Hyperparameter Configuration[/bold]\n"
|
|
172
|
+
"[dim]Press Enter to accept the default for each setting.[/dim]",
|
|
173
|
+
expand=False,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
params = {}
|
|
178
|
+
|
|
179
|
+
def _ask_int(key: str, question: str) -> int:
|
|
180
|
+
default = defaults.get(key)
|
|
181
|
+
raw = questionary.text(
|
|
182
|
+
f"{question} [default: {default}]:",
|
|
183
|
+
default=str(default),
|
|
184
|
+
validate=lambda v: v.isdigit() and int(v) >= 1 or f"Must be a positive integer",
|
|
185
|
+
).ask()
|
|
186
|
+
return int(raw)
|
|
187
|
+
|
|
188
|
+
def _ask_float(key: str, question: str, min_val: float = 0.0) -> float:
|
|
189
|
+
default = defaults.get(key)
|
|
190
|
+
raw = questionary.text(
|
|
191
|
+
f"{question} [default: {default}]:",
|
|
192
|
+
default=str(default),
|
|
193
|
+
validate=lambda v: _valid_float(v, min_val),
|
|
194
|
+
).ask()
|
|
195
|
+
return float(raw)
|
|
196
|
+
|
|
197
|
+
def _valid_float(v: str, min_val: float) -> bool | str:
|
|
198
|
+
try:
|
|
199
|
+
f = float(v)
|
|
200
|
+
return f > min_val or f"Must be > {min_val}"
|
|
201
|
+
except ValueError:
|
|
202
|
+
return "Must be a number"
|
|
203
|
+
|
|
204
|
+
params["num_train_epochs"] = _ask_int("num_train_epochs", "Training epochs")
|
|
205
|
+
params["per_device_train_batch_size"] = _ask_int(
|
|
206
|
+
"per_device_train_batch_size", "Batch size per device"
|
|
207
|
+
)
|
|
208
|
+
params["gradient_accumulation_steps"] = _ask_int(
|
|
209
|
+
"gradient_accumulation_steps", "Gradient accumulation steps"
|
|
210
|
+
)
|
|
211
|
+
params["learning_rate"] = _ask_float("learning_rate", "Learning rate", min_val=0.0)
|
|
212
|
+
params["lora_r"] = _ask_int("lora_r", "LoRA rank (r)")
|
|
213
|
+
params["lora_alpha"] = _ask_int("lora_alpha", "LoRA alpha")
|
|
214
|
+
params["max_seq_length"] = _ask_int("max_seq_length", "Max sequence length")
|
|
215
|
+
|
|
216
|
+
quant_choices = [
|
|
217
|
+
questionary.Choice("4-bit (QLoRA, saves VRAM)", value="4bit"),
|
|
218
|
+
questionary.Choice("8-bit (moderate savings)", value="8bit"),
|
|
219
|
+
questionary.Choice("None (full precision / no bitsandbytes required)", value="none"),
|
|
220
|
+
]
|
|
221
|
+
default_quant = "4bit" if defaults.get("use_4bit") else ("8bit" if defaults.get("use_8bit") else "none")
|
|
222
|
+
quant = questionary.select(
|
|
223
|
+
f"Quantization [default: {default_quant}]:",
|
|
224
|
+
choices=quant_choices,
|
|
225
|
+
default=quant_choices[["4bit", "8bit", "none"].index(default_quant)],
|
|
226
|
+
).ask()
|
|
227
|
+
params["use_4bit"] = quant == "4bit"
|
|
228
|
+
params["use_8bit"] = quant == "8bit"
|
|
229
|
+
|
|
230
|
+
return params
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def prompt_confirm_config(config: dict) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Show a summary table of the final config and ask for confirmation.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
config: Full training configuration dictionary.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if confirmed, False if cancelled.
|
|
242
|
+
"""
|
|
243
|
+
table = Table(title="Training Configuration", show_header=True, header_style="bold cyan")
|
|
244
|
+
table.add_column("Setting", style="dim", width=30)
|
|
245
|
+
table.add_column("Value")
|
|
246
|
+
|
|
247
|
+
display_keys = [
|
|
248
|
+
("task", "Task"),
|
|
249
|
+
("model_name", "Base model"),
|
|
250
|
+
("provider", "Provider"),
|
|
251
|
+
("strategy", "Strategy"),
|
|
252
|
+
("dataset", "Dataset"),
|
|
253
|
+
("compute_specs", "Compute profile"),
|
|
254
|
+
("num_train_epochs", "Epochs"),
|
|
255
|
+
("per_device_train_batch_size", "Batch size"),
|
|
256
|
+
("gradient_accumulation_steps", "Gradient accum. steps"),
|
|
257
|
+
("learning_rate", "Learning rate"),
|
|
258
|
+
("lora_r", "LoRA rank"),
|
|
259
|
+
("lora_alpha", "LoRA alpha"),
|
|
260
|
+
("max_seq_length", "Max seq length"),
|
|
261
|
+
("use_4bit", "4-bit quantization"),
|
|
262
|
+
("use_8bit", "8-bit quantization"),
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
for key, label in display_keys:
|
|
266
|
+
if key in config:
|
|
267
|
+
table.add_row(label, str(config[key]))
|
|
268
|
+
|
|
269
|
+
console.print(table)
|
|
270
|
+
|
|
271
|
+
return questionary.confirm(
|
|
272
|
+
"Start training with this configuration?", default=True
|
|
273
|
+
).ask()
|