Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/together/cli/api/finetune.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import click
from click.core import ParameterSource # type: ignore[attr-defined]
from rich import print as rprint
from rich.json import JSON
from tabulate import tabulate

from together import Together
from together.cli.api.utils import BOOL_WITH_AUTO, INT_WITH_MAX
from together.cli.api.utils import BOOL_WITH_AUTO, INT_WITH_MAX, generate_progress_bar
from together.types.finetune import (
DownloadCheckpointType,
FinetuneEventType,
Expand Down Expand Up @@ -435,6 +436,9 @@ def list(ctx: click.Context) -> None:
"Price": f"""${
finetune_price_to_dollars(float(str(i.total_price)))
}""", # convert to string for mypy typing
"Progress": generate_progress_bar(
i, datetime.now().astimezone(), use_rich=False
),
}
)
table = tabulate(display_list, headers="keys", tablefmt="grid", showindex=True)
Expand All @@ -454,7 +458,15 @@ def retrieve(ctx: click.Context, fine_tune_id: str) -> None:
# remove events from response for cleaner output
response.events = None

click.echo(json.dumps(response.model_dump(exclude_none=True), indent=4))
rprint(JSON.from_data(response.model_dump(exclude_none=True)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we switch to rprint in spirit of depreciation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, yes.

progress_text = generate_progress_bar(
response, datetime.now().astimezone(), use_rich=True
)
status = "Unknown"
if response.status is not None:
status = response.status.value
prefix = f"Status: [bold]{status}[/bold],"
rprint(f"{prefix} {progress_text}")


@fine_tuning.command()
Expand Down
88 changes: 88 additions & 0 deletions src/together/cli/api/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from __future__ import annotations

import math
import re
from gettext import gettext as _
from typing import Literal
from datetime import datetime

import click

from together.types.finetune import FinetuneResponse, COMPLETED_STATUSES

_PROGRESS_BAR_WIDTH = 40


class AutoIntParamType(click.ParamType):
name = "integer_or_max"
Expand Down Expand Up @@ -49,3 +56,84 @@ def convert(

INT_WITH_MAX = AutoIntParamType()
BOOL_WITH_AUTO = BooleanWithAutoParamType()


def _human_readable_time(timedelta: float) -> str:
"""Convert a timedelta to a compact human-readble string
Examples:
00:00:10 -> 10s
01:23:45 -> 1h 23min 45s
1 Month 23 days 04:56:07 -> 1month 23d 4h 56min 7s
Args:
timedelta (float): The timedelta in seconds to convert.
Returns:
A string representing the timedelta in a human-readable format.
"""
units = [
(30 * 24 * 60 * 60, "month"), # 30 days
(24 * 60 * 60, "d"),
(60 * 60, "h"),
(60, "min"),
(1, "s"),
]

total_seconds = int(timedelta)
parts = []

for unit_seconds, unit_name in units:
if total_seconds >= unit_seconds:
value = total_seconds // unit_seconds
total_seconds %= unit_seconds
parts.append(f"{value}{unit_name}")

return " ".join(parts) if parts else "0s"


def generate_progress_bar(
finetune_job: FinetuneResponse, current_time: datetime, use_rich: bool = False
) -> str:
"""Generate a progress bar for a finetune job.
Args:
finetune_job: The finetune job to generate a progress bar for.
current_time: The current time.
use_rich: Whether to use rich formatting.
Returns:
A string representing the progress bar.
"""
progress = "Progress: [bold red]unavailable[/bold red]"
if finetune_job.status in COMPLETED_STATUSES:
progress = "Progress: [bold green]completed[/bold green]"
elif finetune_job.updated_at is not None:
# Replace 'Z' with '+00:00' for Python 3.10 compatibility
updated_at_str = finetune_job.updated_at.replace("Z", "+00:00")
update_at = datetime.fromisoformat(updated_at_str).astimezone()

if finetune_job.progress is not None:
if current_time < update_at:
return progress

if not finetune_job.progress.estimate_available:
return progress

if finetune_job.progress.seconds_remaining <= 0:
return progress

elapsed_time = (current_time - update_at).total_seconds()
ratio_filled = min(
elapsed_time / finetune_job.progress.seconds_remaining, 1.0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we get this progress from API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline, we don't

)
percentage = ratio_filled * 100
filled = math.ceil(ratio_filled * _PROGRESS_BAR_WIDTH)
bar = "█" * filled + "░" * (_PROGRESS_BAR_WIDTH - filled)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might there be off-the-shelf libs to use for this?

time_left = "N/A"
if finetune_job.progress.seconds_remaining > elapsed_time:
time_left = _human_readable_time(
finetune_job.progress.seconds_remaining - elapsed_time
)
time_text = f"{time_left} left"
progress = f"Progress: {bar} [bold]{percentage:>3.0f}%[/bold] [yellow]{time_text}[/yellow]"

if use_rich:
return progress

return re.sub(r"\[/?[^\]]+\]", "", progress)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to produce desired output format directly to avoid regexs?

19 changes: 19 additions & 0 deletions src/together/types/finetune.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ class FinetuneJobStatus(str, Enum):
STATUS_COMPLETED = "completed"


COMPLETED_STATUSES = [
FinetuneJobStatus.STATUS_ERROR,
FinetuneJobStatus.STATUS_USER_ERROR,
FinetuneJobStatus.STATUS_COMPLETED,
FinetuneJobStatus.STATUS_CANCELLED,
]


class FinetuneEventLevels(str, Enum):
"""
Fine-tune job event status levels
Expand Down Expand Up @@ -167,6 +175,15 @@ class TrainingMethodDPO(TrainingMethod):
simpo_gamma: float | None = None


class FinetuneProgress(BaseModel):
"""
Fine-tune job progress
"""

estimate_available: bool = False
seconds_remaining: float = 0


class FinetuneRequest(BaseModel):
"""
Fine-tune request type
Expand Down Expand Up @@ -297,6 +314,8 @@ class FinetuneResponse(BaseModel):
train_on_inputs: StrictBool | Literal["auto"] | None = "auto"
from_checkpoint: str | None = None

progress: FinetuneProgress | None = None

@field_validator("training_type")
@classmethod
def validate_training_type(cls, v: TrainingType) -> TrainingType:
Expand Down
Loading