Skip to content

Commit 136b566

Browse files
cursor[bot]cursoragentblainekasten
authored
feat(cli): add fine-tuning preview command (#466)
* feat(cli): add fine-tuning preview command Co-authored-by: Blaine Kasten <blainekasten@gmail.com> * fix(cli): polish fine-tuning preview command Co-authored-by: Blaine Kasten <blainekasten@gmail.com> * fix(cli): type preview token formatting Co-authored-by: Blaine Kasten <blainekasten@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Blaine Kasten <blainekasten@gmail.com>
1 parent b43099d commit 136b566

5 files changed

Lines changed: 219 additions & 0 deletions

File tree

src/together/lib/cli/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
JIG_VOLUMES_CREATE_HELP_EXAMPLES,
6868
JIG_VOLUMES_UPDATE_HELP_EXAMPLES,
6969
BETA_MODELS_CONFIGS_HELP_EXAMPLES,
70+
FINE_TUNING_PREVIEW_HELP_EXAMPLES,
7071
BETA_CLUSTERS_CREATE_HELP_EXAMPLES,
7172
BETA_CLUSTERS_UPDATE_HELP_EXAMPLES,
7273
BETA_MODELS_DOWNLOAD_HELP_EXAMPLES,
@@ -466,6 +467,11 @@ async def run_command() -> None:
466467
help="Retrieve training metrics for a fine-tuning job",
467468
help_epilogue=FINE_TUNING_LIST_METRICS_HELP_EXAMPLES,
468469
)
470+
fine_tuning_app.command(
471+
(f"{_CLI}.fine_tuning.preview:preview"),
472+
help="Preview how a fine-tuning training file will be tokenized",
473+
help_epilogue=FINE_TUNING_PREVIEW_HELP_EXAMPLES,
474+
)
469475

470476
## Models API commands
471477
models_app = app.command(App(name="models", help="List and upload models", help_epilogue=MODELS_HELP_EXAMPLES))
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal, Optional, Annotated
4+
5+
from cyclopts import Parameter
6+
from rich.markup import escape as escape_rich_markup
7+
8+
from together import omit
9+
from together._utils._json import openapi_dumps
10+
from together.lib.cli.utils.config import CLIConfigParameter
11+
from together.lib.cli.utils._console import console
12+
from together.lib.cli.components.list import ListTable
13+
from together.lib.cli.components.loader import show_loading_status
14+
from together.types.fine_tune_preview_row import FineTunePreviewRow
15+
16+
_TOKEN_PREVIEW_LIMIT = 32
17+
18+
19+
def _format_token(token: str) -> str:
20+
return escape_rich_markup(token.encode("unicode_escape").decode("ascii"))
21+
22+
23+
def _format_tokens(row: FineTunePreviewRow) -> str:
24+
tokens = row.tokens[:_TOKEN_PREVIEW_LIMIT]
25+
labels = row.labels[:_TOKEN_PREVIEW_LIMIT]
26+
formatted: list[str] = []
27+
for token, label in zip(tokens, labels):
28+
token_text = _format_token(token)
29+
formatted.append(f"[dim]{token_text}[/dim]" if label == -100 else token_text)
30+
31+
if len(row.tokens) > _TOKEN_PREVIEW_LIMIT:
32+
formatted.append("[dim]...[/dim]")
33+
34+
return " ".join(formatted)
35+
36+
37+
def _format_spans(row: FineTunePreviewRow) -> str:
38+
if not row.trained_spans:
39+
return "-"
40+
return ", ".join(f"{start}-{end}" for start, end in row.trained_spans)
41+
42+
43+
async def preview(
44+
training_file: Annotated[
45+
str,
46+
Parameter(
47+
alias="-t",
48+
help="Training file ID from the Files API to sample for preview",
49+
),
50+
],
51+
model: Annotated[
52+
str,
53+
Parameter(
54+
alias="-M",
55+
help="Name of the base model whose tokenizer and chat template will be used",
56+
),
57+
],
58+
top_k: Annotated[
59+
Optional[int],
60+
Parameter(help="Maximum number of rows from the start of the training file to tokenize"),
61+
] = None,
62+
train_on_inputs: Annotated[
63+
Optional[bool],
64+
Parameter(help="Whether prompt or user-message tokens should contribute to training loss"),
65+
] = None,
66+
training_method: Annotated[
67+
Literal["sft"],
68+
Parameter(help="Fine-tuning method to preview; only supervised fine-tuning is currently supported"),
69+
] = "sft",
70+
*,
71+
config: CLIConfigParameter,
72+
) -> None:
73+
"""Preview how a fine-tuning training file will be tokenized."""
74+
response = await show_loading_status(
75+
"Loading fine-tuning preview...",
76+
config.client.fine_tuning.preview(
77+
model=model,
78+
training_file=training_file,
79+
top_k=top_k if top_k is not None else omit,
80+
train_on_inputs=train_on_inputs if train_on_inputs is not None else omit,
81+
training_method=training_method,
82+
),
83+
)
84+
85+
if config.json:
86+
console.print_json(openapi_dumps(response).decode("utf-8"))
87+
return
88+
89+
console.print(f"[dim][primary]Model:[/primary][/dim]\t\t[bold]{escape_rich_markup(response.model)}[/bold]")
90+
console.print(f"[dim][primary]Dataset format:[/primary][/dim]\t{response.dataset_format}")
91+
console.print(f"[dim][primary]Max sequence:[/primary][/dim]\t{response.max_seq_length}")
92+
console.print(f"[dim][primary]Train inputs:[/primary][/dim]\t{response.train_on_inputs}")
93+
94+
table = ListTable("Preview Rows", empty_message="No preview rows returned")
95+
table.add_primary_column("Row")
96+
table.add_column("Tokens", justify="right")
97+
table.add_column("Trained", justify="right")
98+
table.add_column("Truncated")
99+
table.add_column("Trained Spans")
100+
table.add_column("Token Preview", ratio=4)
101+
102+
for index, row in enumerate(response.rows, start=1):
103+
table.add_row(
104+
str(index),
105+
str(row.num_tokens),
106+
str(row.num_trained_tokens),
107+
"yes" if row.truncated else "no",
108+
_format_spans(row),
109+
_format_tokens(row),
110+
)
111+
112+
console.print(table)

src/together/lib/cli/utils/_help_examples.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@
9191
9292
[dim]-[/dim] Cancel a fine-tuning job:
9393
[primary]tg ft cancel <ft-job-id>[/primary]
94+
95+
[dim]-[/dim] Preview how a training file will be tokenized:
96+
[primary]tg ft preview --model Qwen/Qwen2-1.5B --training-file <file-id>[/primary]
9497
"""
9598

9699
FINE_TUNING_CREATE_HELP_EXAMPLES = """[dim]Examples:[/dim]
@@ -127,6 +130,17 @@
127130
[primary]tg ft list-metrics <ft-job-id> > plots.txt[/primary]
128131
"""
129132

133+
FINE_TUNING_PREVIEW_HELP_EXAMPLES = """[dim]Examples:[/dim]
134+
[dim]-[/dim] Preview tokenization for the first rows in a training file:
135+
[primary]tg ft preview --model Qwen/Qwen2-1.5B --training-file <file-id>[/primary]
136+
137+
[dim]-[/dim] Preview more rows and include prompt tokens in training loss:
138+
[primary]tg ft preview -M Qwen/Qwen2-1.5B -t <file-id> --top-k 10 --train-on-inputs[/primary]
139+
140+
[dim]-[/dim] Save the raw preview response:
141+
[primary]tg ft preview -M Qwen/Qwen2-1.5B -t <file-id> --json > preview.json[/primary]
142+
"""
143+
130144
FINE_TUNING_DOWNLOAD_HELP_EXAMPLES = """[dim]Examples:[/dim]
131145
[dim]-[/dim] Download a fine-tuned model's weights:
132146
[primary]tg ft download <ft-job-id> --output-dir ./my-model[/primary]

tests/cli/test_fine_tuning.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@
8585
]
8686
}
8787

88+
_FT_PREVIEW_BODY = {
89+
"dataset_format": "conversation",
90+
"max_seq_length": 4096,
91+
"model": "meta-llama/Llama-3-8b",
92+
"train_on_inputs": False,
93+
"rows": [
94+
{
95+
"input_ids": [1, 2, 3],
96+
"labels": [-100, 2, 3],
97+
"num_tokens": 3,
98+
"num_trained_tokens": 2,
99+
"tokens": ["<s>", " hello", " world"],
100+
"trained_spans": [[1, 3]],
101+
"truncated": False,
102+
}
103+
],
104+
}
105+
88106
_MODEL_LIMITS_BODY = {
89107
"max_num_epochs": 10,
90108
"max_learning_rate": 1,
@@ -397,6 +415,74 @@ def test_list_metrics_json_includes_zero_step_filters(self, respx_mock: MockRout
397415
assert json.loads(result.output) == _FT_METRICS_BODY["metrics"]
398416

399417

418+
class TestFineTuningPreview:
419+
@pytest.mark.respx(base_url=base_url)
420+
@pytest.mark.parametrize(
421+
("train_on_inputs_flag", "expected_train_on_inputs"),
422+
[
423+
("--train-on-inputs", True),
424+
("--no-train-on-inputs", False),
425+
],
426+
)
427+
def test_preview_json_sends_params(
428+
self,
429+
respx_mock: MockRouter,
430+
cli_runner: CliRunner,
431+
train_on_inputs_flag: str,
432+
expected_train_on_inputs: bool,
433+
) -> None:
434+
route = respx_mock.post("/fine-tunes/preview").mock(return_value=httpx.Response(200, json=_FT_PREVIEW_BODY))
435+
436+
result = cli_runner.invoke(
437+
[
438+
"fine-tuning",
439+
"preview",
440+
"--training-file",
441+
"file-train",
442+
"--model",
443+
"meta-llama/Llama-3-8b",
444+
"--top-k",
445+
"5",
446+
train_on_inputs_flag,
447+
"--training-method",
448+
"sft",
449+
"--json",
450+
]
451+
)
452+
453+
assert result.exit_code == 0
454+
assert json.loads(result.output) == _FT_PREVIEW_BODY
455+
request_body = json.loads(cast(Call, route.calls[0]).request.content)
456+
assert request_body == {
457+
"model": "meta-llama/Llama-3-8b",
458+
"training_file": "file-train",
459+
"top_k": 5,
460+
"train_on_inputs": expected_train_on_inputs,
461+
"training_method": "sft",
462+
}
463+
464+
@pytest.mark.respx(base_url=base_url)
465+
def test_preview_table(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None:
466+
respx_mock.post("/fine-tunes/preview").mock(return_value=httpx.Response(200, json=_FT_PREVIEW_BODY))
467+
468+
result = cli_runner.invoke(
469+
[
470+
"fine-tuning",
471+
"preview",
472+
"--training-file",
473+
"file-train",
474+
"--model",
475+
"meta-llama/Llama-3-8b",
476+
]
477+
)
478+
479+
assert result.exit_code == 0
480+
assert "conversation" in result.output
481+
assert "Preview Rows" in result.output
482+
assert "1-3" in result.output
483+
assert "hello" in result.output
484+
485+
400486
class TestFineTuningDownload:
401487
@pytest.mark.respx(base_url=base_url)
402488
def test_download_invokes_download_manager(

tests/cli/test_json_mode_pipeable_to_jq.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def test_fine_tuning_json_mode(self) -> None:
126126
fine_tuning.run_and_assert("delete ft-123 --force")
127127
fine_tuning.run_and_assert("list-events ft-123")
128128
fine_tuning.run_and_assert("list-checkpoints ft-123")
129+
fine_tuning.run_and_assert("preview --training-file file-123 --model deepseek-ai/DeepSeek-R1")
129130
fine_tuning.run_and_assert("retrieve-checkpoint ft-123/checkpoint-123")
130131
fine_tuning.run_and_assert("retrieve-checkpoint ft-123/checkpoint-123")
131132

0 commit comments

Comments
 (0)