troy-cli 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: troy-cli
3
- Version: 0.2.1
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.2.1"
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
 
@@ -1,3 +1,3 @@
1
1
  """Troy: fine-tune LLMs on your MacBook with one YAML file."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"
@@ -120,7 +120,12 @@ def doctor() -> None:
120
120
  except ImportError:
121
121
  table.add_row("mlx-lm", f"{fail} (not installed)")
122
122
 
123
- table.add_row("Fine-tunable models", model_guidance(hw.memory_gb))
123
+ table.add_row("Comfortable size", model_guidance(hw.memory_gb))
124
+ table.add_row(
125
+ "Supported models",
126
+ "any mlx-lm architecture (Llama, Qwen, Gemma, Phi, Mistral, ...)\n"
127
+ "thousands of ready conversions: hf.co/mlx-community",
128
+ )
124
129
  console.print(table)
125
130
 
126
131
  if not hw.is_apple_silicon:
@@ -137,6 +142,19 @@ def train(
137
142
  from .data import load_and_prepare
138
143
 
139
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
+
140
158
  train_records, valid_records, fmt = load_and_prepare(
141
159
  cfg.data, cfg.task, cfg.training.seed
142
160
  )
@@ -174,6 +192,7 @@ def chat(
174
192
  max_tokens: int = typer.Option(512),
175
193
  temperature: float = typer.Option(0.7),
176
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)."),
177
196
  ) -> None:
178
197
  """Chat with your fine-tuned model."""
179
198
  _require_apple_silicon()
@@ -193,6 +212,14 @@ def chat(
193
212
  console.print(
194
213
  "[yellow]No trained adapter found — chatting with the base model.[/yellow]"
195
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
196
223
  run_chat(model, adapter, max_tokens, temperature, prompt)
197
224
 
198
225
 
@@ -42,7 +42,8 @@ def detect() -> Hardware:
42
42
  )
43
43
 
44
44
 
45
- # (min unified memory GB, guidance) — QLoRA 4-bit fine-tuning headroom.
45
+ # (min unified memory GB, guidance) — QLoRA 4-bit sizing EXAMPLES, not a
46
+ # whitelist: any mlx-lm-supported architecture trains.
46
47
  MODEL_GUIDANCE = [
47
48
  (128, "up to ~70B (4-bit QLoRA), e.g. Llama-3.3-70B, Qwen2.5-72B"),
48
49
  (64, "up to ~32B (4-bit QLoRA), e.g. Qwen2.5-32B, Gemma-2-27B"),
@@ -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