troy-cli 0.1.2__tar.gz → 0.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.
- {troy_cli-0.1.2 → troy_cli-0.2.0}/PKG-INFO +6 -4
- {troy_cli-0.1.2 → troy_cli-0.2.0}/README.md +5 -3
- {troy_cli-0.1.2 → troy_cli-0.2.0}/pyproject.toml +1 -1
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/__init__.py +1 -1
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/cli.py +45 -1
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/config.py +7 -1
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/data.py +5 -3
- troy_cli-0.2.0/src/troy/evaluate.py +88 -0
- troy_cli-0.2.0/src/troy/push.py +25 -0
- troy_cli-0.2.0/src/troy/train_orpo.py +119 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/tests/test_config.py +8 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/.gitignore +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/chat.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/export.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/hardware.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/serve.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/templates.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/train_dpo.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/src/troy/train_sft.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.0}/tests/test_data.py +0 -0
- {troy_cli-0.1.2 → troy_cli-0.2.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.2.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
|
|
@@ -36,7 +36,7 @@ need a workstation GPU anywhere else.
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
brew install avirajkhare00/troy/troy
|
|
39
|
-
# or
|
|
39
|
+
# or: pipx install troy-cli
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
## Quickstart
|
|
@@ -54,7 +54,7 @@ troy export -f gguf # ship it to llama.cpp / Ollama / LM Studio
|
|
|
54
54
|
|
|
55
55
|
```yaml
|
|
56
56
|
base: mlx-community/Qwen3-0.6B-4bit
|
|
57
|
-
task: sft # or: dpo
|
|
57
|
+
task: sft # or: dpo, orpo
|
|
58
58
|
|
|
59
59
|
data:
|
|
60
60
|
train: ./data/train.jsonl # alpaca, sharegpt, chat, completions, text — auto-detected
|
|
@@ -77,10 +77,12 @@ output: ./output
|
|
|
77
77
|
|---|---|
|
|
78
78
|
| `troy init` | Create a config from a template (`chat`, `dpo`) |
|
|
79
79
|
| `troy doctor` | Hardware + dependency check, with model-size guidance |
|
|
80
|
-
| `troy train` | LoRA fine-tuning: SFT or
|
|
80
|
+
| `troy train` | LoRA fine-tuning: SFT, DPO, or ORPO |
|
|
81
81
|
| `troy chat` | Interactive REPL (or `-p` for one-shot) with your adapter |
|
|
82
|
+
| `troy eval` | Base-vs-tuned val loss, perplexity, side-by-side samples |
|
|
82
83
|
| `troy serve` | OpenAI-compatible API server for your model |
|
|
83
84
|
| `troy export` | Fuse the adapter; export MLX or GGUF |
|
|
85
|
+
| `troy push` | Upload adapter or fused model to the Hugging Face Hub |
|
|
84
86
|
| `troy data inspect` | Dataset stats and format detection |
|
|
85
87
|
|
|
86
88
|
## What Troy can train on your Mac
|
|
@@ -21,7 +21,7 @@ need a workstation GPU anywhere else.
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
brew install avirajkhare00/troy/troy
|
|
24
|
-
# or
|
|
24
|
+
# or: pipx install troy-cli
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Quickstart
|
|
@@ -39,7 +39,7 @@ troy export -f gguf # ship it to llama.cpp / Ollama / LM Studio
|
|
|
39
39
|
|
|
40
40
|
```yaml
|
|
41
41
|
base: mlx-community/Qwen3-0.6B-4bit
|
|
42
|
-
task: sft # or: dpo
|
|
42
|
+
task: sft # or: dpo, orpo
|
|
43
43
|
|
|
44
44
|
data:
|
|
45
45
|
train: ./data/train.jsonl # alpaca, sharegpt, chat, completions, text — auto-detected
|
|
@@ -62,10 +62,12 @@ output: ./output
|
|
|
62
62
|
|---|---|
|
|
63
63
|
| `troy init` | Create a config from a template (`chat`, `dpo`) |
|
|
64
64
|
| `troy doctor` | Hardware + dependency check, with model-size guidance |
|
|
65
|
-
| `troy train` | LoRA fine-tuning: SFT or
|
|
65
|
+
| `troy train` | LoRA fine-tuning: SFT, DPO, or ORPO |
|
|
66
66
|
| `troy chat` | Interactive REPL (or `-p` for one-shot) with your adapter |
|
|
67
|
+
| `troy eval` | Base-vs-tuned val loss, perplexity, side-by-side samples |
|
|
67
68
|
| `troy serve` | OpenAI-compatible API server for your model |
|
|
68
69
|
| `troy export` | Fuse the adapter; export MLX or GGUF |
|
|
70
|
+
| `troy push` | Upload adapter or fused model to the Hugging Face Hub |
|
|
69
71
|
| `troy data inspect` | Dataset stats and format detection |
|
|
70
72
|
|
|
71
73
|
## What Troy can train on your Mac
|
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
import platform
|
|
7
7
|
import sys
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
9
|
+
from typing import List, Optional
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
from rich.console import Console
|
|
@@ -149,6 +149,10 @@ def train(
|
|
|
149
149
|
from .train_sft import run_sft
|
|
150
150
|
|
|
151
151
|
run_sft(cfg, train_records, valid_records)
|
|
152
|
+
elif cfg.task == "orpo":
|
|
153
|
+
from .train_orpo import run_orpo
|
|
154
|
+
|
|
155
|
+
run_orpo(cfg, train_records, valid_records)
|
|
152
156
|
else:
|
|
153
157
|
from .train_dpo import run_dpo
|
|
154
158
|
|
|
@@ -248,6 +252,46 @@ def export(
|
|
|
248
252
|
run_export(cfg.base, str(cfg.adapter_path), str(out), fmt, dequantize)
|
|
249
253
|
|
|
250
254
|
|
|
255
|
+
@app.command()
|
|
256
|
+
def eval(
|
|
257
|
+
config: Path = typer.Option(Path("troy.yaml"), "--config", "-c", help="Config file."),
|
|
258
|
+
prompts: Optional[List[str]] = typer.Option(
|
|
259
|
+
None, "--prompt", "-p",
|
|
260
|
+
help="Prompt for side-by-side base-vs-tuned generation (repeatable).",
|
|
261
|
+
),
|
|
262
|
+
max_tokens: int = typer.Option(200),
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Compare the trained adapter against the base model (loss, ppl, samples)."""
|
|
265
|
+
_require_apple_silicon()
|
|
266
|
+
from .config import load_config
|
|
267
|
+
from .data import load_and_prepare
|
|
268
|
+
from .evaluate import run_eval
|
|
269
|
+
|
|
270
|
+
cfg = load_config(config)
|
|
271
|
+
_, valid_records, _ = load_and_prepare(cfg.data, cfg.task, cfg.training.seed)
|
|
272
|
+
run_eval(cfg, valid_records, list(prompts) if prompts else None, max_tokens)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.command()
|
|
276
|
+
def push(
|
|
277
|
+
repo: str = typer.Argument(help="Hub repo id, e.g. username/my-model."),
|
|
278
|
+
config: Path = typer.Option(Path("troy.yaml"), "--config", "-c", help="Config file."),
|
|
279
|
+
fused: bool = typer.Option(False, help="Push the fused model instead of the adapter."),
|
|
280
|
+
public: bool = typer.Option(False, help="Make the Hub repo public."),
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Upload your trained adapter (or fused model) to the Hugging Face Hub."""
|
|
283
|
+
from .config import load_config
|
|
284
|
+
from .push import run_push
|
|
285
|
+
|
|
286
|
+
cfg = load_config(config)
|
|
287
|
+
folder = (cfg.output_path / "fused") if fused else cfg.adapter_path
|
|
288
|
+
if not folder.exists():
|
|
289
|
+
what = "troy export" if fused else "troy train"
|
|
290
|
+
console.print(f"[red]{folder} not found. Run `{what}` first.[/red]")
|
|
291
|
+
raise typer.Exit(1)
|
|
292
|
+
run_push(folder, repo, private=not public)
|
|
293
|
+
|
|
294
|
+
|
|
251
295
|
@app.command()
|
|
252
296
|
def data(
|
|
253
297
|
action: str = typer.Argument(help="Action: inspect"),
|
|
@@ -24,6 +24,11 @@ class DpoConfig(BaseModel):
|
|
|
24
24
|
beta: float = 0.1
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class OrpoConfig(BaseModel):
|
|
28
|
+
lam: float = Field(0.1, alias="lambda")
|
|
29
|
+
model_config = {"populate_by_name": True}
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
class DataConfig(BaseModel):
|
|
28
33
|
train: str
|
|
29
34
|
valid: Optional[str] = None
|
|
@@ -42,6 +47,7 @@ class TrainingConfig(BaseModel):
|
|
|
42
47
|
seq_len: int = 2048
|
|
43
48
|
lora: LoraConfig = LoraConfig()
|
|
44
49
|
dpo: DpoConfig = DpoConfig()
|
|
50
|
+
orpo: OrpoConfig = OrpoConfig()
|
|
45
51
|
grad_checkpoint: bool = False
|
|
46
52
|
grad_accumulation_steps: int = 1
|
|
47
53
|
save_every: int = 100
|
|
@@ -50,7 +56,7 @@ class TrainingConfig(BaseModel):
|
|
|
50
56
|
|
|
51
57
|
class TroyConfig(BaseModel):
|
|
52
58
|
base: str
|
|
53
|
-
task: Literal["sft", "dpo"] = "sft"
|
|
59
|
+
task: Literal["sft", "dpo", "orpo"] = "sft"
|
|
54
60
|
data: DataConfig
|
|
55
61
|
training: TrainingConfig = TrainingConfig()
|
|
56
62
|
output: str = "./output"
|
|
@@ -117,13 +117,15 @@ def load_and_prepare(
|
|
|
117
117
|
|
|
118
118
|
fmt = cfg.format if cfg.format != "auto" else detect_format(records[0])
|
|
119
119
|
|
|
120
|
-
if task
|
|
120
|
+
if task in ("dpo", "orpo") and fmt != "preference":
|
|
121
121
|
raise ValueError(
|
|
122
|
-
f"Task `
|
|
122
|
+
f"Task `{task}` needs preference data (prompt/chosen/rejected); "
|
|
123
123
|
f"detected format `{fmt}`."
|
|
124
124
|
)
|
|
125
125
|
if task == "sft" and fmt == "preference":
|
|
126
|
-
raise ValueError(
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"Preference data detected — set `task: dpo` (or `task: orpo`) in troy.yaml."
|
|
128
|
+
)
|
|
127
129
|
|
|
128
130
|
if fmt == "preference":
|
|
129
131
|
records = [_normalize_preference(r) for r in records]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Evaluate a trained adapter: base vs tuned perplexity and side-by-side samples.
|
|
2
|
+
|
|
3
|
+
The base-model numbers reuse the zero-scale trick: with every LoRA scale set
|
|
4
|
+
to 0 the tuned model *is* the base model, so both sides come from one load.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import types
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
import mlx.core as mx
|
|
14
|
+
from mlx_lm.generate import generate
|
|
15
|
+
from mlx_lm.sample_utils import make_sampler
|
|
16
|
+
from mlx_lm.tuner.datasets import CacheDataset, create_dataset
|
|
17
|
+
from mlx_lm.tuner.trainer import default_loss, evaluate, iterate_batches
|
|
18
|
+
from mlx_lm.utils import load
|
|
19
|
+
|
|
20
|
+
from .config import TroyConfig
|
|
21
|
+
from .train_dpo import _ReferenceMode
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _val_loss(model, dataset, batch_size: int, seq_len: int) -> float:
|
|
25
|
+
return float(
|
|
26
|
+
evaluate(
|
|
27
|
+
model=model,
|
|
28
|
+
dataset=CacheDataset(dataset),
|
|
29
|
+
batch_size=batch_size,
|
|
30
|
+
num_batches=-1,
|
|
31
|
+
max_seq_length=seq_len,
|
|
32
|
+
loss=default_loss,
|
|
33
|
+
iterate_batches=iterate_batches,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_eval(
|
|
39
|
+
config: TroyConfig,
|
|
40
|
+
valid_records: List[Dict[str, Any]],
|
|
41
|
+
sample_prompts: Optional[List[str]] = None,
|
|
42
|
+
max_tokens: int = 200,
|
|
43
|
+
) -> None:
|
|
44
|
+
adapter_file = config.adapter_path / "adapters.safetensors"
|
|
45
|
+
if not adapter_file.exists():
|
|
46
|
+
raise SystemExit(f"No adapter at {adapter_file}. Run `troy train` first.")
|
|
47
|
+
|
|
48
|
+
print(f"Loading {config.base} + adapter ...")
|
|
49
|
+
model, tokenizer = load(config.base, adapter_path=str(config.adapter_path))
|
|
50
|
+
|
|
51
|
+
results = {}
|
|
52
|
+
if valid_records and config.task == "sft":
|
|
53
|
+
ds_config = types.SimpleNamespace(mask_prompt=config.data.mask_prompt)
|
|
54
|
+
dataset = create_dataset(valid_records, tokenizer, ds_config)
|
|
55
|
+
bs = max(1, min(4, len(valid_records)))
|
|
56
|
+
|
|
57
|
+
tuned = _val_loss(model, dataset, bs, config.training.seq_len)
|
|
58
|
+
with _ReferenceMode(model):
|
|
59
|
+
base = _val_loss(model, dataset, bs, config.training.seq_len)
|
|
60
|
+
results = {
|
|
61
|
+
"val records": len(valid_records),
|
|
62
|
+
"base loss": f"{base:.3f}",
|
|
63
|
+
"tuned loss": f"{tuned:.3f}",
|
|
64
|
+
"base ppl": f"{math.exp(base):.1f}",
|
|
65
|
+
"tuned ppl": f"{math.exp(tuned):.1f}",
|
|
66
|
+
}
|
|
67
|
+
for k, v in results.items():
|
|
68
|
+
print(f" {k:>12}: {v}")
|
|
69
|
+
delta = base - tuned
|
|
70
|
+
print(f" {'Δ loss':>12}: {delta:+.3f} ({'tuned better' if delta > 0 else 'base better'})")
|
|
71
|
+
elif config.task != "sft":
|
|
72
|
+
print("(loss comparison is defined for task: sft — showing generations only)")
|
|
73
|
+
|
|
74
|
+
if sample_prompts:
|
|
75
|
+
sampler = make_sampler(temp=0.0)
|
|
76
|
+
print("\n--- side-by-side generations (temperature 0) ---")
|
|
77
|
+
for prompt in sample_prompts:
|
|
78
|
+
templated = tokenizer.apply_chat_template(
|
|
79
|
+
[{"role": "user", "content": prompt}],
|
|
80
|
+
add_generation_prompt=True,
|
|
81
|
+
return_dict=False,
|
|
82
|
+
)
|
|
83
|
+
tuned_out = generate(model, tokenizer, templated, max_tokens=max_tokens, sampler=sampler)
|
|
84
|
+
with _ReferenceMode(model):
|
|
85
|
+
base_out = generate(model, tokenizer, templated, max_tokens=max_tokens, sampler=sampler)
|
|
86
|
+
print(f"\n>> {prompt}")
|
|
87
|
+
print(f"[base] {base_out.strip()[:400]}")
|
|
88
|
+
print(f"[tuned] {tuned_out.strip()[:400]}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Upload a trained adapter or fused model to the Hugging Face Hub."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_push(folder: Path, repo: str, private: bool = True) -> str:
|
|
9
|
+
from huggingface_hub import HfApi
|
|
10
|
+
|
|
11
|
+
api = HfApi()
|
|
12
|
+
try:
|
|
13
|
+
who = api.whoami()
|
|
14
|
+
except Exception:
|
|
15
|
+
raise SystemExit(
|
|
16
|
+
"Not logged in to Hugging Face. Run: hf auth login "
|
|
17
|
+
"(or set HF_TOKEN) and retry."
|
|
18
|
+
)
|
|
19
|
+
print(f"Logged in as {who['name']}. Uploading {folder} -> {repo} "
|
|
20
|
+
f"({'private' if private else 'public'}) ...")
|
|
21
|
+
api.create_repo(repo, exist_ok=True, private=private)
|
|
22
|
+
api.upload_folder(folder_path=str(folder), repo_id=repo)
|
|
23
|
+
url = f"https://huggingface.co/{repo}"
|
|
24
|
+
print(f"Done: {url}")
|
|
25
|
+
return url
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""ORPO: Odds Ratio Preference Optimization on Apple Silicon.
|
|
2
|
+
|
|
3
|
+
ORPO needs no reference model at all — the loss combines a standard SFT term
|
|
4
|
+
on the chosen response with an odds-ratio penalty pushing chosen above
|
|
5
|
+
rejected. One model in memory, one pass per completion.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
import mlx.core as mx
|
|
14
|
+
import mlx.nn as nn
|
|
15
|
+
import mlx.optimizers as optim
|
|
16
|
+
from mlx.utils import tree_flatten
|
|
17
|
+
from mlx_lm.tuner.trainer import grad_checkpoint
|
|
18
|
+
from mlx_lm.utils import load
|
|
19
|
+
|
|
20
|
+
from .config import TroyConfig
|
|
21
|
+
from .train_dpo import _batch, _encode_pair
|
|
22
|
+
from .train_sft import apply_lora, resolve_batch_size, resolve_iters, save_adapter_config
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _mean_logps(model, tokens: mx.array, mask: mx.array):
|
|
26
|
+
"""Per-sequence mean log-prob over completion tokens (and the sum + count)."""
|
|
27
|
+
logits = model(tokens[:, :-1])
|
|
28
|
+
targets = tokens[:, 1:]
|
|
29
|
+
logps = -nn.losses.cross_entropy(logits, targets, reduction="none")
|
|
30
|
+
total = (logps * mask).sum(axis=-1)
|
|
31
|
+
count = mx.maximum(mask.sum(axis=-1), 1)
|
|
32
|
+
return total / count, total, count
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_orpo(
|
|
36
|
+
config: TroyConfig,
|
|
37
|
+
train_records: List[Dict[str, Any]],
|
|
38
|
+
valid_records: List[Dict[str, Any]],
|
|
39
|
+
) -> None:
|
|
40
|
+
mx.random.seed(config.training.seed)
|
|
41
|
+
print(f"Loading {config.base} ...")
|
|
42
|
+
model, tokenizer = load(config.base)
|
|
43
|
+
|
|
44
|
+
batch_size = resolve_batch_size(config)
|
|
45
|
+
if config.training.batch_size == "auto":
|
|
46
|
+
batch_size = max(1, batch_size // 2) # chosen + rejected per example
|
|
47
|
+
iters = resolve_iters(config, len(train_records), batch_size)
|
|
48
|
+
num_layers = apply_lora(model, config)
|
|
49
|
+
save_adapter_config(config, num_layers)
|
|
50
|
+
if config.training.grad_checkpoint:
|
|
51
|
+
grad_checkpoint(model.layers[0])
|
|
52
|
+
|
|
53
|
+
lam = config.training.orpo.lam
|
|
54
|
+
max_len = config.training.seq_len
|
|
55
|
+
pad_id = tokenizer.pad_token_id or tokenizer.eos_token_id or 0
|
|
56
|
+
|
|
57
|
+
encoded = [
|
|
58
|
+
(
|
|
59
|
+
_encode_pair(tokenizer, r["prompt"], r["chosen"], max_len),
|
|
60
|
+
_encode_pair(tokenizer, r["prompt"], r["rejected"], max_len),
|
|
61
|
+
)
|
|
62
|
+
for r in train_records
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def loss_fn(model, tc, mc, tr, mr):
|
|
66
|
+
mean_c, total_c, count_c = _mean_logps(model, tc, mc)
|
|
67
|
+
mean_r, _, _ = _mean_logps(model, tr, mr)
|
|
68
|
+
# log odds: log(p/(1-p)) with p = exp(mean logp)
|
|
69
|
+
log_odds = (mean_c - mean_r) - (
|
|
70
|
+
mx.log1p(-mx.exp(mx.minimum(mean_c, -1e-6)))
|
|
71
|
+
- mx.log1p(-mx.exp(mx.minimum(mean_r, -1e-6)))
|
|
72
|
+
)
|
|
73
|
+
or_loss = -nn.log_sigmoid(log_odds).mean()
|
|
74
|
+
sft_loss = -(total_c.sum() / count_c.sum())
|
|
75
|
+
reward_acc = (mean_c > mean_r).mean()
|
|
76
|
+
return sft_loss + lam * or_loss, reward_acc
|
|
77
|
+
|
|
78
|
+
loss_and_grad = nn.value_and_grad(model, loss_fn)
|
|
79
|
+
opt = optim.Adam(learning_rate=config.training.lr)
|
|
80
|
+
|
|
81
|
+
print(
|
|
82
|
+
f"Training: task=orpo lambda={lam} batch_size={batch_size} iters={iters} "
|
|
83
|
+
f"lr={config.training.lr} pairs={len(encoded)}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
config.adapter_path.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
adapter_file = config.adapter_path / "adapters.safetensors"
|
|
88
|
+
n = len(encoded)
|
|
89
|
+
losses, accs = [], []
|
|
90
|
+
start = time.time()
|
|
91
|
+
|
|
92
|
+
for it in range(iters):
|
|
93
|
+
idx = [(it * batch_size + k) % n for k in range(batch_size)]
|
|
94
|
+
tc, mc = _batch([encoded[i][0] for i in idx], pad_id)
|
|
95
|
+
tr, mr = _batch([encoded[i][1] for i in idx], pad_id)
|
|
96
|
+
|
|
97
|
+
(loss, acc), grads = loss_and_grad(model, tc, mc, tr, mr)
|
|
98
|
+
opt.update(model, grads)
|
|
99
|
+
mx.eval(model.parameters(), opt.state, loss)
|
|
100
|
+
losses.append(loss.item())
|
|
101
|
+
accs.append(acc.item())
|
|
102
|
+
|
|
103
|
+
if (it + 1) % 10 == 0 or it == iters - 1:
|
|
104
|
+
speed = (it + 1) / (time.time() - start)
|
|
105
|
+
print(
|
|
106
|
+
f"Iter {it + 1}/{iters}: loss {sum(losses)/len(losses):.4f}, "
|
|
107
|
+
f"reward acc {sum(accs)/len(accs):.3f}, {speed:.2f} it/s"
|
|
108
|
+
)
|
|
109
|
+
losses, accs = [], []
|
|
110
|
+
|
|
111
|
+
if (it + 1) % config.training.save_every == 0:
|
|
112
|
+
_save(model, adapter_file)
|
|
113
|
+
|
|
114
|
+
_save(model, adapter_file)
|
|
115
|
+
print(f"\nDone. Adapter saved to {config.adapter_path}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _save(model, adapter_file) -> None:
|
|
119
|
+
mx.save_safetensors(str(adapter_file), dict(tree_flatten(model.trainable_parameters())))
|
|
@@ -49,3 +49,11 @@ def test_missing_file_message(tmp_path):
|
|
|
49
49
|
def test_adapter_path_under_output():
|
|
50
50
|
cfg = TroyConfig.model_validate({**BASE, "output": "/tmp/run1"})
|
|
51
51
|
assert str(cfg.adapter_path) == "/tmp/run1/adapter"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_orpo_config():
|
|
55
|
+
cfg = TroyConfig.model_validate(
|
|
56
|
+
{**BASE, "task": "orpo", "training": {"orpo": {"lambda": 0.2}}}
|
|
57
|
+
)
|
|
58
|
+
assert cfg.task == "orpo"
|
|
59
|
+
assert cfg.training.orpo.lam == 0.2
|
|
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
|