troy-cli 0.2.2__tar.gz → 0.3.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.
- {troy_cli-0.2.2 → troy_cli-0.3.0}/PKG-INFO +4 -2
- {troy_cli-0.2.2 → troy_cli-0.3.0}/README.md +1 -1
- {troy_cli-0.2.2 → troy_cli-0.3.0}/pyproject.toml +4 -1
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/__init__.py +1 -1
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/cli.py +22 -0
- troy_cli-0.3.0/src/troy/train_vision.py +94 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/.gitignore +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/chat.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/config.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/data.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/evaluate.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/export.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/hardware.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/push.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/serve.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/templates.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/train_dpo.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/train_orpo.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/src/troy/train_sft.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/tests/test_config.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/tests/test_data.py +0 -0
- {troy_cli-0.2.2 → troy_cli-0.3.0}/tests/test_hardware.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: troy-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Fine-tune LLMs on your MacBook with one YAML file. Built for Apple Silicon.
|
|
5
5
|
Author: Troy
|
|
6
6
|
License: Apache-2.0
|
|
@@ -11,6 +11,8 @@ Requires-Dist: pydantic>=2.5
|
|
|
11
11
|
Requires-Dist: pyyaml>=6.0
|
|
12
12
|
Requires-Dist: rich>=13.0
|
|
13
13
|
Requires-Dist: typer>=0.12
|
|
14
|
+
Provides-Extra: vision
|
|
15
|
+
Requires-Dist: mlx-vlm[train]>=0.7; extra == 'vision'
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
15
17
|
|
|
16
18
|
# Troy
|
|
@@ -79,7 +81,7 @@ output: ./output
|
|
|
79
81
|
|---|---|
|
|
80
82
|
| `troy init` | Create a config from a template (`chat`, `dpo`, `orpo`) |
|
|
81
83
|
| `troy doctor` | Hardware + dependency check, with model-size guidance |
|
|
82
|
-
| `troy train` | LoRA fine-tuning: SFT, DPO, or ORPO |
|
|
84
|
+
| `troy train` | LoRA fine-tuning: SFT, DPO, or ORPO — text, or vision with `[vision]` extra |
|
|
83
85
|
| `troy chat` | Interactive REPL (or `-p` for one-shot) with your adapter |
|
|
84
86
|
| `troy eval` | Base-vs-tuned val loss, perplexity, side-by-side samples |
|
|
85
87
|
| `troy serve` | OpenAI-compatible API server for your model |
|
|
@@ -64,7 +64,7 @@ output: ./output
|
|
|
64
64
|
|---|---|
|
|
65
65
|
| `troy init` | Create a config from a template (`chat`, `dpo`, `orpo`) |
|
|
66
66
|
| `troy doctor` | Hardware + dependency check, with model-size guidance |
|
|
67
|
-
| `troy train` | LoRA fine-tuning: SFT, DPO, or ORPO |
|
|
67
|
+
| `troy train` | LoRA fine-tuning: SFT, DPO, or ORPO — text, or vision with `[vision]` extra |
|
|
68
68
|
| `troy chat` | Interactive REPL (or `-p` for one-shot) with your adapter |
|
|
69
69
|
| `troy eval` | Base-vs-tuned val loss, perplexity, side-by-side samples |
|
|
70
70
|
| `troy serve` | OpenAI-compatible API server for your model |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "troy-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Fine-tune LLMs on your MacBook with one YAML file. Built for Apple Silicon."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -15,6 +15,9 @@ dependencies = [
|
|
|
15
15
|
"rich>=13.0",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
vision = ["mlx-vlm[train]>=0.7"]
|
|
20
|
+
|
|
18
21
|
[project.scripts]
|
|
19
22
|
troy = "troy.cli:app"
|
|
20
23
|
|
|
@@ -142,6 +142,19 @@ def train(
|
|
|
142
142
|
from .data import load_and_prepare
|
|
143
143
|
|
|
144
144
|
cfg = load_config(config)
|
|
145
|
+
|
|
146
|
+
if Path(cfg.data.train).expanduser().is_dir(): # folder of images => vision
|
|
147
|
+
if cfg.task != "sft":
|
|
148
|
+
console.print("[red]Vision fine-tuning supports task: sft (for now).[/red]")
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
from .train_vision import run_vision_sft
|
|
151
|
+
|
|
152
|
+
run_vision_sft(cfg)
|
|
153
|
+
console.print(
|
|
154
|
+
'\nTry it: [bold]troy chat --image photo.png -p "your question"[/bold]'
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
145
158
|
train_records, valid_records, fmt = load_and_prepare(
|
|
146
159
|
cfg.data, cfg.task, cfg.training.seed
|
|
147
160
|
)
|
|
@@ -179,6 +192,7 @@ def chat(
|
|
|
179
192
|
max_tokens: int = typer.Option(512),
|
|
180
193
|
temperature: float = typer.Option(0.7),
|
|
181
194
|
prompt: Optional[str] = typer.Option(None, "--prompt", "-p", help="One-shot prompt (no REPL)."),
|
|
195
|
+
image: Optional[Path] = typer.Option(None, help="Image for a vision model (one-shot; needs -p)."),
|
|
182
196
|
) -> None:
|
|
183
197
|
"""Chat with your fine-tuned model."""
|
|
184
198
|
_require_apple_silicon()
|
|
@@ -198,6 +212,14 @@ def chat(
|
|
|
198
212
|
console.print(
|
|
199
213
|
"[yellow]No trained adapter found — chatting with the base model.[/yellow]"
|
|
200
214
|
)
|
|
215
|
+
if image is not None:
|
|
216
|
+
if prompt is None:
|
|
217
|
+
console.print("[red]--image needs a one-shot prompt: -p \"your question\"[/red]")
|
|
218
|
+
raise typer.Exit(1)
|
|
219
|
+
from .train_vision import run_vision_chat
|
|
220
|
+
|
|
221
|
+
run_vision_chat(model, adapter, str(image), prompt, max_tokens, temperature)
|
|
222
|
+
return
|
|
201
223
|
run_chat(model, adapter, max_tokens, temperature, prompt)
|
|
202
224
|
|
|
203
225
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Vision-language fine-tuning (wraps mlx-vlm's LoRA trainer).
|
|
2
|
+
|
|
3
|
+
Contract: `data.train` is a FOLDER containing images plus a metadata.jsonl
|
|
4
|
+
with {"file_name", "question", "answer"} rows (HF imagefolder format).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .config import TroyConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ensure_mlx_vlm() -> None:
|
|
19
|
+
try:
|
|
20
|
+
import mlx_vlm # noqa: F401
|
|
21
|
+
except ImportError:
|
|
22
|
+
raise SystemExit(
|
|
23
|
+
"Vision fine-tuning needs mlx-vlm. Install with:\n"
|
|
24
|
+
" pip install 'troy-cli[vision]'"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def count_records(data_dir: Path) -> int:
|
|
29
|
+
meta = data_dir / "metadata.jsonl"
|
|
30
|
+
if not meta.exists():
|
|
31
|
+
raise SystemExit(
|
|
32
|
+
f"{data_dir} has no metadata.jsonl. Vision data is a folder of images "
|
|
33
|
+
'plus metadata.jsonl rows: {"file_name": ..., "question": ..., "answer": ...}'
|
|
34
|
+
)
|
|
35
|
+
with open(meta) as f:
|
|
36
|
+
rows = [json.loads(line) for line in f if line.strip()]
|
|
37
|
+
if rows and not {"file_name", "question", "answer"} <= set(rows[0]):
|
|
38
|
+
raise SystemExit(
|
|
39
|
+
"metadata.jsonl rows need file_name, question, and answer fields."
|
|
40
|
+
)
|
|
41
|
+
return len(rows)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_vision_sft(config: TroyConfig) -> None:
|
|
45
|
+
ensure_mlx_vlm()
|
|
46
|
+
data_dir = Path(config.data.train).expanduser()
|
|
47
|
+
n = count_records(data_dir)
|
|
48
|
+
print(f"Vision data: {n} images (folder: {data_dir})")
|
|
49
|
+
|
|
50
|
+
t = config.training
|
|
51
|
+
batch_size = 1 if t.batch_size == "auto" else int(t.batch_size)
|
|
52
|
+
iters = t.iters or max(1, math.ceil((t.epochs or 3) * n / batch_size))
|
|
53
|
+
|
|
54
|
+
config.adapter_path.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
cmd = [
|
|
56
|
+
sys.executable, "-m", "mlx_vlm.lora",
|
|
57
|
+
"--model-path", config.base,
|
|
58
|
+
"--dataset", str(data_dir),
|
|
59
|
+
"--split", "train",
|
|
60
|
+
"--batch-size", str(batch_size),
|
|
61
|
+
"--iters", str(iters),
|
|
62
|
+
"--learning-rate", str(t.lr),
|
|
63
|
+
"--lora-rank", str(t.lora.r),
|
|
64
|
+
"--lora-alpha", str(t.lora.alpha),
|
|
65
|
+
"--lora-dropout", str(t.lora.dropout),
|
|
66
|
+
"--max-seq-length", str(t.seq_len),
|
|
67
|
+
"--steps-per-save", str(t.save_every),
|
|
68
|
+
"--output-path", str(config.adapter_path),
|
|
69
|
+
]
|
|
70
|
+
if t.grad_checkpoint:
|
|
71
|
+
cmd.append("--grad-checkpoint")
|
|
72
|
+
|
|
73
|
+
print(f"Training: task=sft (vision) batch_size={batch_size} iters={iters} lr={t.lr}")
|
|
74
|
+
result = subprocess.run(cmd)
|
|
75
|
+
if result.returncode != 0:
|
|
76
|
+
raise SystemExit(result.returncode)
|
|
77
|
+
print(f"\nDone. Adapter saved to {config.adapter_path}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_vision_chat(
|
|
81
|
+
model: str, adapter: str | None, image: str, prompt: str, max_tokens: int, temperature: float
|
|
82
|
+
) -> None:
|
|
83
|
+
ensure_mlx_vlm()
|
|
84
|
+
cmd = [
|
|
85
|
+
sys.executable, "-m", "mlx_vlm", "generate",
|
|
86
|
+
"--model", model,
|
|
87
|
+
"--image", image,
|
|
88
|
+
"--prompt", prompt,
|
|
89
|
+
"--max-tokens", str(max_tokens),
|
|
90
|
+
"--temperature", str(temperature),
|
|
91
|
+
]
|
|
92
|
+
if adapter:
|
|
93
|
+
cmd += ["--adapter-path", adapter]
|
|
94
|
+
raise SystemExit(subprocess.run(cmd).returncode)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|