together 2.0.0a9__py3-none-any.whl → 2.0.0a11__py3-none-any.whl
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.
- together/_base_client.py +8 -2
- together/_types.py +3 -2
- together/_version.py +1 -1
- together/lib/cli/api/fine_tuning.py +76 -5
- together/lib/cli/api/models.py +1 -6
- together/lib/cli/api/utils.py +97 -8
- together/lib/constants.py +3 -0
- together/lib/resources/files.py +65 -6
- together/lib/resources/fine_tuning.py +41 -2
- together/lib/types/fine_tuning.py +19 -0
- together/resources/audio/transcriptions.py +6 -4
- together/resources/audio/translations.py +6 -4
- together/resources/chat/completions.py +48 -0
- together/resources/fine_tuning.py +213 -5
- together/resources/models.py +41 -5
- together/types/__init__.py +3 -0
- together/types/audio/transcription_create_params.py +5 -2
- together/types/audio/translation_create_params.py +5 -2
- together/types/audio/voice_list_response.py +4 -0
- together/types/autoscaling.py +2 -0
- together/types/autoscaling_param.py +2 -0
- together/types/chat/completion_create_params.py +78 -5
- together/types/dedicated_endpoint.py +2 -0
- together/types/endpoint_list_avzones_response.py +2 -0
- together/types/endpoint_list_response.py +2 -0
- together/types/execute_response.py +7 -0
- together/types/fine_tuning_cancel_response.py +20 -0
- together/types/fine_tuning_estimate_price_params.py +98 -0
- together/types/fine_tuning_estimate_price_response.py +24 -0
- together/types/fine_tuning_list_response.py +20 -0
- together/types/finetune_response.py +17 -2
- together/types/hardware_list_response.py +8 -0
- together/types/model_list_params.py +12 -0
- together/types/video_job.py +8 -0
- {together-2.0.0a9.dist-info → together-2.0.0a11.dist-info}/METADATA +9 -11
- {together-2.0.0a9.dist-info → together-2.0.0a11.dist-info}/RECORD +39 -37
- together/lib/resources/models.py +0 -35
- {together-2.0.0a9.dist-info → together-2.0.0a11.dist-info}/WHEEL +0 -0
- {together-2.0.0a9.dist-info → together-2.0.0a11.dist-info}/entry_points.txt +0 -0
- {together-2.0.0a9.dist-info → together-2.0.0a11.dist-info}/licenses/LICENSE +0 -0
together/_base_client.py
CHANGED
|
@@ -1247,9 +1247,12 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
|
|
|
1247
1247
|
*,
|
|
1248
1248
|
cast_to: Type[ResponseT],
|
|
1249
1249
|
body: Body | None = None,
|
|
1250
|
+
files: RequestFiles | None = None,
|
|
1250
1251
|
options: RequestOptions = {},
|
|
1251
1252
|
) -> ResponseT:
|
|
1252
|
-
opts = FinalRequestOptions.construct(
|
|
1253
|
+
opts = FinalRequestOptions.construct(
|
|
1254
|
+
method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
|
|
1255
|
+
)
|
|
1253
1256
|
return self.request(cast_to, opts)
|
|
1254
1257
|
|
|
1255
1258
|
def put(
|
|
@@ -1767,9 +1770,12 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
|
|
|
1767
1770
|
*,
|
|
1768
1771
|
cast_to: Type[ResponseT],
|
|
1769
1772
|
body: Body | None = None,
|
|
1773
|
+
files: RequestFiles | None = None,
|
|
1770
1774
|
options: RequestOptions = {},
|
|
1771
1775
|
) -> ResponseT:
|
|
1772
|
-
opts = FinalRequestOptions.construct(
|
|
1776
|
+
opts = FinalRequestOptions.construct(
|
|
1777
|
+
method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
|
|
1778
|
+
)
|
|
1773
1779
|
return await self.request(cast_to, opts)
|
|
1774
1780
|
|
|
1775
1781
|
async def put(
|
together/_types.py
CHANGED
|
@@ -243,6 +243,9 @@ _T_co = TypeVar("_T_co", covariant=True)
|
|
|
243
243
|
if TYPE_CHECKING:
|
|
244
244
|
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
|
|
245
245
|
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
|
|
246
|
+
#
|
|
247
|
+
# Note: index() and count() methods are intentionally omitted to allow pyright to properly
|
|
248
|
+
# infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
|
|
246
249
|
class SequenceNotStr(Protocol[_T_co]):
|
|
247
250
|
@overload
|
|
248
251
|
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
|
|
@@ -251,8 +254,6 @@ if TYPE_CHECKING:
|
|
|
251
254
|
def __contains__(self, value: object, /) -> bool: ...
|
|
252
255
|
def __len__(self) -> int: ...
|
|
253
256
|
def __iter__(self) -> Iterator[_T_co]: ...
|
|
254
|
-
def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
|
|
255
|
-
def count(self, value: Any, /) -> int: ...
|
|
256
257
|
def __reversed__(self) -> Iterator[_T_co]: ...
|
|
257
258
|
else:
|
|
258
259
|
# just point this to a normal `Sequence` at runtime to avoid having to special case
|
together/_version.py
CHANGED
|
@@ -10,13 +10,15 @@ from textwrap import wrap
|
|
|
10
10
|
import click
|
|
11
11
|
from rich import print as rprint
|
|
12
12
|
from tabulate import tabulate
|
|
13
|
+
from rich.json import JSON
|
|
13
14
|
from click.core import ParameterSource # type: ignore[attr-defined]
|
|
14
15
|
|
|
15
16
|
from together import Together
|
|
17
|
+
from together.types import fine_tuning_estimate_price_params as pe_params
|
|
16
18
|
from together._types import NOT_GIVEN, NotGiven
|
|
17
19
|
from together.lib.utils import log_warn
|
|
18
20
|
from together.lib.utils.tools import format_timestamp, finetune_price_to_dollars
|
|
19
|
-
from together.lib.cli.api.utils import INT_WITH_MAX, BOOL_WITH_AUTO
|
|
21
|
+
from together.lib.cli.api.utils import INT_WITH_MAX, BOOL_WITH_AUTO, generate_progress_bar
|
|
20
22
|
from together.lib.resources.files import DownloadManager
|
|
21
23
|
from together.lib.utils.serializer import datetime_serializer
|
|
22
24
|
from together.types.finetune_response import TrainingTypeFullTrainingType, TrainingTypeLoRaTrainingType
|
|
@@ -24,13 +26,21 @@ from together.lib.resources.fine_tuning import get_model_limits
|
|
|
24
26
|
|
|
25
27
|
_CONFIRMATION_MESSAGE = (
|
|
26
28
|
"You are about to create a fine-tuning job. "
|
|
27
|
-
"The
|
|
29
|
+
"The estimated price of this job is {price}. "
|
|
30
|
+
"The actual cost of your job will be determined by the model size, the number of tokens "
|
|
28
31
|
"in the training file, the number of tokens in the validation file, the number of epochs, and "
|
|
29
|
-
"the number of evaluations. Visit https://www.together.ai/pricing to
|
|
32
|
+
"the number of evaluations. Visit https://www.together.ai/pricing to learn more about pricing.\n"
|
|
33
|
+
"{warning}"
|
|
30
34
|
"You can pass `-y` or `--confirm` to your command to skip this message.\n\n"
|
|
31
35
|
"Do you want to proceed?"
|
|
32
36
|
)
|
|
33
37
|
|
|
38
|
+
_WARNING_MESSAGE_INSUFFICIENT_FUNDS = (
|
|
39
|
+
"The estimated price of this job is significantly greater than your current credit limit and balance combined. "
|
|
40
|
+
"It will likely get cancelled due to insufficient funds. "
|
|
41
|
+
"Consider increasing your credit limit at https://api.together.xyz/settings/profile\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
34
44
|
_FT_JOB_WITH_STEP_REGEX = r"^ft-[\dabcdef-]+:\d+$"
|
|
35
45
|
|
|
36
46
|
|
|
@@ -323,7 +333,60 @@ def create(
|
|
|
323
333
|
elif n_evals > 0 and not validation_file:
|
|
324
334
|
raise click.BadParameter("You have specified a number of evaluation loops but no validation file.")
|
|
325
335
|
|
|
326
|
-
|
|
336
|
+
training_type_cls: pe_params.TrainingType
|
|
337
|
+
if lora:
|
|
338
|
+
training_type_cls = pe_params.TrainingTypeLoRaTrainingType(
|
|
339
|
+
lora_alpha=int(lora_alpha or 0),
|
|
340
|
+
lora_r=lora_r or 0,
|
|
341
|
+
lora_dropout=lora_dropout or 0,
|
|
342
|
+
lora_trainable_modules=lora_trainable_modules or "all-linear",
|
|
343
|
+
type="Lora",
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
training_type_cls = pe_params.TrainingTypeFullTrainingType(
|
|
347
|
+
type="Full",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
training_method_cls: pe_params.TrainingMethod
|
|
351
|
+
if training_method == "sft":
|
|
352
|
+
training_method_cls = pe_params.TrainingMethodTrainingMethodSft(
|
|
353
|
+
method="sft",
|
|
354
|
+
train_on_inputs=train_on_inputs or "auto",
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
training_method_cls = pe_params.TrainingMethodTrainingMethodDpo(
|
|
358
|
+
method="dpo",
|
|
359
|
+
dpo_beta=dpo_beta or 0,
|
|
360
|
+
dpo_normalize_logratios_by_length=dpo_normalize_logratios_by_length or False,
|
|
361
|
+
dpo_reference_free=False,
|
|
362
|
+
rpo_alpha=rpo_alpha or 0,
|
|
363
|
+
simpo_gamma=simpo_gamma or 0,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
finetune_price_estimation_result = client.fine_tuning.estimate_price(
|
|
367
|
+
training_file=training_file,
|
|
368
|
+
validation_file=validation_file,
|
|
369
|
+
model=model or "",
|
|
370
|
+
n_epochs=n_epochs,
|
|
371
|
+
n_evals=n_evals,
|
|
372
|
+
training_type=training_type_cls,
|
|
373
|
+
training_method=training_method_cls,
|
|
374
|
+
)
|
|
375
|
+
price = click.style(
|
|
376
|
+
f"${finetune_price_estimation_result.estimated_total_price:.2f}",
|
|
377
|
+
bold=True,
|
|
378
|
+
)
|
|
379
|
+
if not finetune_price_estimation_result.allowed_to_proceed:
|
|
380
|
+
warning = click.style(_WARNING_MESSAGE_INSUFFICIENT_FUNDS, fg="red", bold=True)
|
|
381
|
+
else:
|
|
382
|
+
warning = ""
|
|
383
|
+
|
|
384
|
+
confirmation_message = _CONFIRMATION_MESSAGE.format(
|
|
385
|
+
price=price,
|
|
386
|
+
warning=warning,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if confirm or click.confirm(confirmation_message, default=True, show_default=True):
|
|
327
390
|
response = client.fine_tuning.create(
|
|
328
391
|
**training_args,
|
|
329
392
|
verbose=True,
|
|
@@ -363,6 +426,9 @@ def list(ctx: click.Context) -> None:
|
|
|
363
426
|
"Price": f"""${
|
|
364
427
|
finetune_price_to_dollars(float(str(i.total_price)))
|
|
365
428
|
}""", # convert to string for mypy typing
|
|
429
|
+
"Progress": generate_progress_bar(
|
|
430
|
+
i, datetime.now().astimezone(), use_rich=False
|
|
431
|
+
),
|
|
366
432
|
}
|
|
367
433
|
)
|
|
368
434
|
table = tabulate(display_list, headers="keys", tablefmt="grid", showindex=True)
|
|
@@ -382,7 +448,12 @@ def retrieve(ctx: click.Context, fine_tune_id: str) -> None:
|
|
|
382
448
|
# remove events from response for cleaner output
|
|
383
449
|
response.events = None
|
|
384
450
|
|
|
385
|
-
|
|
451
|
+
rprint(JSON.from_data(response.model_json_schema()))
|
|
452
|
+
progress_text = generate_progress_bar(
|
|
453
|
+
response, datetime.now().astimezone(), use_rich=True
|
|
454
|
+
)
|
|
455
|
+
prefix = f"Status: [bold]{response.status}[/bold],"
|
|
456
|
+
rprint(f"{prefix} {progress_text}")
|
|
386
457
|
|
|
387
458
|
|
|
388
459
|
@fine_tuning.command()
|
together/lib/cli/api/models.py
CHANGED
|
@@ -7,7 +7,6 @@ from tabulate import tabulate
|
|
|
7
7
|
from together import Together, omit
|
|
8
8
|
from together._models import BaseModel
|
|
9
9
|
from together._response import APIResponse as APIResponse
|
|
10
|
-
from together.lib.resources.models import filter_by_dedicated_models
|
|
11
10
|
from together.types.model_upload_response import ModelUploadResponse
|
|
12
11
|
|
|
13
12
|
|
|
@@ -34,11 +33,7 @@ def list(ctx: click.Context, type: Optional[str], json: bool) -> None:
|
|
|
34
33
|
"""List models"""
|
|
35
34
|
client: Together = ctx.obj
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
models_list = response
|
|
39
|
-
|
|
40
|
-
if type == "dedicated":
|
|
41
|
-
models_list = filter_by_dedicated_models(client, models_list)
|
|
36
|
+
models_list = client.models.list(dedicated=type == "dedicated" if type else omit)
|
|
42
37
|
|
|
43
38
|
display_list: List[Dict[str, Any]] = []
|
|
44
39
|
model: BaseModel
|
together/lib/cli/api/utils.py
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import re
|
|
4
|
+
import math
|
|
5
|
+
from typing import List, Union, Literal
|
|
4
6
|
from gettext import gettext as _
|
|
5
|
-
from
|
|
7
|
+
from datetime import datetime
|
|
6
8
|
|
|
7
9
|
import click
|
|
8
10
|
|
|
11
|
+
from together.lib.types.fine_tuning import COMPLETED_STATUSES, FinetuneResponse
|
|
12
|
+
from together.types.finetune_response import FinetuneResponse as _FinetuneResponse
|
|
13
|
+
from together.types.fine_tuning_list_response import Data
|
|
14
|
+
|
|
15
|
+
_PROGRESS_BAR_WIDTH = 40
|
|
16
|
+
|
|
9
17
|
|
|
10
18
|
class AutoIntParamType(click.ParamType):
|
|
11
19
|
name = "integer_or_max"
|
|
12
20
|
_number_class = int
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
def convert(
|
|
22
|
+
def convert( # pyright: ignore[reportImplicitOverride]
|
|
16
23
|
self, value: str, param: click.Parameter | None, ctx: click.Context | None
|
|
17
24
|
) -> int | Literal["max"] | None:
|
|
18
25
|
if value == "max":
|
|
@@ -21,7 +28,9 @@ class AutoIntParamType(click.ParamType):
|
|
|
21
28
|
return int(value)
|
|
22
29
|
except ValueError:
|
|
23
30
|
self.fail(
|
|
24
|
-
_("{value!r} is not a valid {number_type}.").format(
|
|
31
|
+
_("{value!r} is not a valid {number_type}.").format(
|
|
32
|
+
value=value, number_type=self.name
|
|
33
|
+
),
|
|
25
34
|
param,
|
|
26
35
|
ctx,
|
|
27
36
|
)
|
|
@@ -30,8 +39,7 @@ class AutoIntParamType(click.ParamType):
|
|
|
30
39
|
class BooleanWithAutoParamType(click.ParamType):
|
|
31
40
|
name = "boolean_or_auto"
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
def convert(
|
|
42
|
+
def convert( # pyright: ignore[reportImplicitOverride]
|
|
35
43
|
self, value: str, param: click.Parameter | None, ctx: click.Context | None
|
|
36
44
|
) -> bool | Literal["auto"] | None:
|
|
37
45
|
if value == "auto":
|
|
@@ -40,7 +48,9 @@ class BooleanWithAutoParamType(click.ParamType):
|
|
|
40
48
|
return bool(value)
|
|
41
49
|
except ValueError:
|
|
42
50
|
self.fail(
|
|
43
|
-
_("{value!r} is not a valid {type}.").format(
|
|
51
|
+
_("{value!r} is not a valid {type}.").format(
|
|
52
|
+
value=value, type=self.name
|
|
53
|
+
),
|
|
44
54
|
param,
|
|
45
55
|
ctx,
|
|
46
56
|
)
|
|
@@ -48,3 +58,82 @@ class BooleanWithAutoParamType(click.ParamType):
|
|
|
48
58
|
|
|
49
59
|
INT_WITH_MAX = AutoIntParamType()
|
|
50
60
|
BOOL_WITH_AUTO = BooleanWithAutoParamType()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _human_readable_time(timedelta: float) -> str:
|
|
64
|
+
"""Convert a timedelta to a compact human-readble string
|
|
65
|
+
Examples:
|
|
66
|
+
00:00:10 -> 10s
|
|
67
|
+
01:23:45 -> 1h 23min 45s
|
|
68
|
+
1 Month 23 days 04:56:07 -> 1month 23d 4h 56min 7s
|
|
69
|
+
Args:
|
|
70
|
+
timedelta (float): The timedelta in seconds to convert.
|
|
71
|
+
Returns:
|
|
72
|
+
A string representing the timedelta in a human-readable format.
|
|
73
|
+
"""
|
|
74
|
+
units = [
|
|
75
|
+
(30 * 24 * 60 * 60, "month"), # 30 days
|
|
76
|
+
(24 * 60 * 60, "d"),
|
|
77
|
+
(60 * 60, "h"),
|
|
78
|
+
(60, "min"),
|
|
79
|
+
(1, "s"),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
total_seconds = int(timedelta)
|
|
83
|
+
parts: List[str] = []
|
|
84
|
+
|
|
85
|
+
for unit_seconds, unit_name in units:
|
|
86
|
+
if total_seconds >= unit_seconds:
|
|
87
|
+
value = total_seconds // unit_seconds
|
|
88
|
+
total_seconds %= unit_seconds
|
|
89
|
+
parts.append(f"{value}{unit_name}")
|
|
90
|
+
|
|
91
|
+
return " ".join(parts) if parts else "0s"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def generate_progress_bar(
|
|
95
|
+
finetune_job: Union[Data, FinetuneResponse, _FinetuneResponse], current_time: datetime, use_rich: bool = False
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Generate a progress bar for a finetune job.
|
|
98
|
+
Args:
|
|
99
|
+
finetune_job: The finetune job to generate a progress bar for.
|
|
100
|
+
current_time: The current time.
|
|
101
|
+
use_rich: Whether to use rich formatting.
|
|
102
|
+
Returns:
|
|
103
|
+
A string representing the progress bar.
|
|
104
|
+
"""
|
|
105
|
+
progress = "Progress: [bold red]unavailable[/bold red]"
|
|
106
|
+
if finetune_job.status in COMPLETED_STATUSES:
|
|
107
|
+
progress = "Progress: [bold green]completed[/bold green]"
|
|
108
|
+
elif finetune_job.updated_at is not None:
|
|
109
|
+
update_at = finetune_job.updated_at.astimezone()
|
|
110
|
+
|
|
111
|
+
if finetune_job.progress is not None:
|
|
112
|
+
if current_time < update_at:
|
|
113
|
+
return progress
|
|
114
|
+
|
|
115
|
+
if not finetune_job.progress.estimate_available:
|
|
116
|
+
return progress
|
|
117
|
+
|
|
118
|
+
if finetune_job.progress.seconds_remaining <= 0:
|
|
119
|
+
return progress
|
|
120
|
+
|
|
121
|
+
elapsed_time = (current_time - update_at).total_seconds()
|
|
122
|
+
ratio_filled = min(
|
|
123
|
+
elapsed_time / finetune_job.progress.seconds_remaining, 1.0
|
|
124
|
+
)
|
|
125
|
+
percentage = ratio_filled * 100
|
|
126
|
+
filled = math.ceil(ratio_filled * _PROGRESS_BAR_WIDTH)
|
|
127
|
+
bar = "█" * filled + "░" * (_PROGRESS_BAR_WIDTH - filled)
|
|
128
|
+
time_left = "N/A"
|
|
129
|
+
if finetune_job.progress.seconds_remaining > elapsed_time:
|
|
130
|
+
time_left = _human_readable_time(
|
|
131
|
+
finetune_job.progress.seconds_remaining - elapsed_time
|
|
132
|
+
)
|
|
133
|
+
time_text = f"{time_left} left"
|
|
134
|
+
progress = f"Progress: {bar} [bold]{percentage:>3.0f}%[/bold] [yellow]{time_text}[/yellow]"
|
|
135
|
+
|
|
136
|
+
if use_rich:
|
|
137
|
+
return progress
|
|
138
|
+
|
|
139
|
+
return re.sub(r"\[/?[^\]]+\]", "", progress)
|
together/lib/constants.py
CHANGED
|
@@ -14,6 +14,9 @@ import enum
|
|
|
14
14
|
# Download defaults
|
|
15
15
|
DOWNLOAD_BLOCK_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
16
16
|
DISABLE_TQDM = False
|
|
17
|
+
MAX_DOWNLOAD_RETRIES = 5 # Maximum retries for download failures
|
|
18
|
+
DOWNLOAD_INITIAL_RETRY_DELAY = 1.0 # Initial retry delay in seconds
|
|
19
|
+
DOWNLOAD_MAX_RETRY_DELAY = 30.0 # Maximum retry delay in seconds
|
|
17
20
|
|
|
18
21
|
# Upload defaults
|
|
19
22
|
MAX_CONCURRENT_PARTS = 4 # Maximum concurrent parts for multipart upload
|
together/lib/resources/files.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import math
|
|
5
5
|
import stat
|
|
6
|
+
import time
|
|
6
7
|
import uuid
|
|
7
8
|
import shutil
|
|
8
9
|
import asyncio
|
|
@@ -29,12 +30,15 @@ from ..constants import (
|
|
|
29
30
|
MAX_MULTIPART_PARTS,
|
|
30
31
|
TARGET_PART_SIZE_MB,
|
|
31
32
|
MAX_CONCURRENT_PARTS,
|
|
33
|
+
MAX_DOWNLOAD_RETRIES,
|
|
32
34
|
MULTIPART_THRESHOLD_GB,
|
|
35
|
+
DOWNLOAD_MAX_RETRY_DELAY,
|
|
33
36
|
MULTIPART_UPLOAD_TIMEOUT,
|
|
37
|
+
DOWNLOAD_INITIAL_RETRY_DELAY,
|
|
34
38
|
)
|
|
35
39
|
from ..._resource import SyncAPIResource, AsyncAPIResource
|
|
36
40
|
from ..types.error import DownloadError, FileTypeError
|
|
37
|
-
from ..._exceptions import APIStatusError, AuthenticationError
|
|
41
|
+
from ..._exceptions import APIStatusError, APIConnectionError, AuthenticationError
|
|
38
42
|
|
|
39
43
|
log: logging.Logger = logging.getLogger(__name__)
|
|
40
44
|
|
|
@@ -198,6 +202,11 @@ class DownloadManager(SyncAPIResource):
|
|
|
198
202
|
|
|
199
203
|
assert file_size != 0, "Unable to retrieve remote file."
|
|
200
204
|
|
|
205
|
+
# Download with retry logic
|
|
206
|
+
bytes_downloaded = 0
|
|
207
|
+
retry_count = 0
|
|
208
|
+
retry_delay = DOWNLOAD_INITIAL_RETRY_DELAY
|
|
209
|
+
|
|
201
210
|
with tqdm(
|
|
202
211
|
total=file_size,
|
|
203
212
|
unit="B",
|
|
@@ -205,14 +214,64 @@ class DownloadManager(SyncAPIResource):
|
|
|
205
214
|
desc=f"Downloading file {file_path.name}",
|
|
206
215
|
disable=bool(DISABLE_TQDM),
|
|
207
216
|
) as pbar:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
while bytes_downloaded < file_size:
|
|
218
|
+
try:
|
|
219
|
+
# If this is a retry, close the previous response and create a new one with Range header
|
|
220
|
+
if bytes_downloaded > 0:
|
|
221
|
+
response.close()
|
|
222
|
+
|
|
223
|
+
log.info(f"Resuming download from byte {bytes_downloaded}")
|
|
224
|
+
response = self._client.get(
|
|
225
|
+
path=url,
|
|
226
|
+
cast_to=httpx.Response,
|
|
227
|
+
stream=True,
|
|
228
|
+
options=RequestOptions(
|
|
229
|
+
headers={"Range": f"bytes={bytes_downloaded}-"},
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Download chunks
|
|
234
|
+
for chunk in response.iter_bytes(DOWNLOAD_BLOCK_SIZE):
|
|
235
|
+
temp_file.write(chunk) # type: ignore
|
|
236
|
+
bytes_downloaded += len(chunk)
|
|
237
|
+
pbar.update(len(chunk))
|
|
238
|
+
|
|
239
|
+
# Successfully completed download
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
except (httpx.RequestError, httpx.StreamError, APIConnectionError) as e:
|
|
243
|
+
if retry_count >= MAX_DOWNLOAD_RETRIES:
|
|
244
|
+
log.error(f"Download failed after {retry_count} retries")
|
|
245
|
+
raise DownloadError(
|
|
246
|
+
f"Download failed after {retry_count} retries. Last error: {str(e)}"
|
|
247
|
+
) from e
|
|
248
|
+
|
|
249
|
+
retry_count += 1
|
|
250
|
+
log.warning(
|
|
251
|
+
f"Download interrupted at {bytes_downloaded}/{file_size} bytes. "
|
|
252
|
+
f"Retry {retry_count}/{MAX_DOWNLOAD_RETRIES} in {retry_delay}s..."
|
|
253
|
+
)
|
|
254
|
+
time.sleep(retry_delay)
|
|
255
|
+
|
|
256
|
+
# Exponential backoff with max delay cap
|
|
257
|
+
retry_delay = min(retry_delay * 2, DOWNLOAD_MAX_RETRY_DELAY)
|
|
258
|
+
|
|
259
|
+
except APIStatusError as e:
|
|
260
|
+
# For API errors, don't retry
|
|
261
|
+
log.error(f"API error during download: {e}")
|
|
262
|
+
raise APIStatusError(
|
|
263
|
+
"Error downloading file",
|
|
264
|
+
response=e.response,
|
|
265
|
+
body=e.response,
|
|
266
|
+
) from e
|
|
267
|
+
|
|
268
|
+
# Close the response
|
|
269
|
+
response.close()
|
|
211
270
|
|
|
212
271
|
# Raise exception if remote file size does not match downloaded file size
|
|
213
272
|
if os.stat(temp_file.name).st_size != file_size:
|
|
214
|
-
DownloadError(
|
|
215
|
-
f"Downloaded file size `{
|
|
273
|
+
raise DownloadError(
|
|
274
|
+
f"Downloaded file size `{bytes_downloaded}` bytes does not match remote file size `{file_size}` bytes."
|
|
216
275
|
)
|
|
217
276
|
|
|
218
277
|
# Moves temp file to output file path
|
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Literal
|
|
|
4
4
|
|
|
5
5
|
from rich import print as rprint
|
|
6
6
|
|
|
7
|
+
from together.types import fine_tuning_estimate_price_params as pe_params
|
|
7
8
|
from together.lib.utils import log_warn_once
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
@@ -66,7 +67,7 @@ def create_finetune_request(
|
|
|
66
67
|
hf_model_revision: str | None = None,
|
|
67
68
|
hf_api_token: str | None = None,
|
|
68
69
|
hf_output_repo_name: str | None = None,
|
|
69
|
-
) -> FinetuneRequest:
|
|
70
|
+
) -> tuple[FinetuneRequest, pe_params.TrainingType, pe_params.TrainingMethod]:
|
|
70
71
|
if model is not None and from_checkpoint is not None:
|
|
71
72
|
raise ValueError("You must specify either a model or a checkpoint to start a job from, not both")
|
|
72
73
|
|
|
@@ -233,8 +234,46 @@ def create_finetune_request(
|
|
|
233
234
|
hf_output_repo_name=hf_output_repo_name,
|
|
234
235
|
)
|
|
235
236
|
|
|
236
|
-
|
|
237
|
+
training_type_pe, training_method_pe = create_price_estimation_params(finetune_request)
|
|
237
238
|
|
|
239
|
+
return finetune_request, training_type_pe, training_method_pe
|
|
240
|
+
|
|
241
|
+
def create_price_estimation_params(finetune_request: FinetuneRequest) -> tuple[pe_params.TrainingType, pe_params.TrainingMethod]:
|
|
242
|
+
training_type_cls: pe_params.TrainingType
|
|
243
|
+
if isinstance(finetune_request.training_type, FullTrainingType):
|
|
244
|
+
training_type_cls = pe_params.TrainingTypeFullTrainingType(
|
|
245
|
+
type="Full",
|
|
246
|
+
)
|
|
247
|
+
elif isinstance(finetune_request.training_type, LoRATrainingType):
|
|
248
|
+
training_type_cls = pe_params.TrainingTypeLoRaTrainingType(
|
|
249
|
+
lora_alpha=finetune_request.training_type.lora_alpha,
|
|
250
|
+
lora_r=finetune_request.training_type.lora_r,
|
|
251
|
+
lora_dropout=finetune_request.training_type.lora_dropout,
|
|
252
|
+
lora_trainable_modules=finetune_request.training_type.lora_trainable_modules,
|
|
253
|
+
type="Lora",
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
raise ValueError(f"Unknown training type: {finetune_request.training_type}")
|
|
257
|
+
|
|
258
|
+
training_method_cls: pe_params.TrainingMethod
|
|
259
|
+
if isinstance(finetune_request.training_method, TrainingMethodSFT):
|
|
260
|
+
training_method_cls = pe_params.TrainingMethodTrainingMethodSft(
|
|
261
|
+
method="sft",
|
|
262
|
+
train_on_inputs=finetune_request.training_method.train_on_inputs,
|
|
263
|
+
)
|
|
264
|
+
elif isinstance(finetune_request.training_method, TrainingMethodDPO):
|
|
265
|
+
training_method_cls = pe_params.TrainingMethodTrainingMethodDpo(
|
|
266
|
+
method="dpo",
|
|
267
|
+
dpo_beta=finetune_request.training_method.dpo_beta or 0,
|
|
268
|
+
dpo_normalize_logratios_by_length=finetune_request.training_method.dpo_normalize_logratios_by_length,
|
|
269
|
+
dpo_reference_free=finetune_request.training_method.dpo_reference_free,
|
|
270
|
+
rpo_alpha=finetune_request.training_method.rpo_alpha or 0,
|
|
271
|
+
simpo_gamma=finetune_request.training_method.simpo_gamma or 0,
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
raise ValueError(f"Unknown training method: {finetune_request.training_method}")
|
|
275
|
+
|
|
276
|
+
return training_type_cls, training_method_cls
|
|
238
277
|
|
|
239
278
|
def get_model_limits(client: Together, model: str) -> FinetuneTrainingLimits:
|
|
240
279
|
"""
|
|
@@ -25,6 +25,14 @@ class FinetuneJobStatus(str, Enum):
|
|
|
25
25
|
STATUS_COMPLETED = "completed"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
COMPLETED_STATUSES = [
|
|
29
|
+
FinetuneJobStatus.STATUS_ERROR,
|
|
30
|
+
FinetuneJobStatus.STATUS_USER_ERROR,
|
|
31
|
+
FinetuneJobStatus.STATUS_COMPLETED,
|
|
32
|
+
FinetuneJobStatus.STATUS_CANCELLED,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
class FinetuneEventType(str, Enum):
|
|
29
37
|
"""
|
|
30
38
|
Fine-tune job event types
|
|
@@ -260,6 +268,15 @@ FinetuneLRScheduler: TypeAlias = Union[
|
|
|
260
268
|
]
|
|
261
269
|
|
|
262
270
|
|
|
271
|
+
class FinetuneProgress(BaseModel):
|
|
272
|
+
"""
|
|
273
|
+
Fine-tune job progress
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
estimate_available: bool = False
|
|
277
|
+
seconds_remaining: float = 0
|
|
278
|
+
|
|
279
|
+
|
|
263
280
|
class FinetuneResponse(BaseModel):
|
|
264
281
|
"""
|
|
265
282
|
Fine-tune API response type
|
|
@@ -393,6 +410,8 @@ class FinetuneResponse(BaseModel):
|
|
|
393
410
|
training_file_size: Optional[int] = Field(None, alias="TrainingFileSize")
|
|
394
411
|
train_on_inputs: Union[StrictBool, Literal["auto"], None] = "auto"
|
|
395
412
|
|
|
413
|
+
progress: Union[FinetuneProgress, None] = None
|
|
414
|
+
|
|
396
415
|
@classmethod
|
|
397
416
|
def validate_training_type(cls, v: TrainingType) -> TrainingType:
|
|
398
417
|
if v.type == "Full" or v.type == "":
|
|
@@ -47,7 +47,7 @@ class TranscriptionsResource(SyncAPIResource):
|
|
|
47
47
|
def create(
|
|
48
48
|
self,
|
|
49
49
|
*,
|
|
50
|
-
file: FileTypes,
|
|
50
|
+
file: Union[FileTypes, str],
|
|
51
51
|
diarize: bool | Omit = omit,
|
|
52
52
|
language: str | Omit = omit,
|
|
53
53
|
max_speakers: int | Omit = omit,
|
|
@@ -68,7 +68,8 @@ class TranscriptionsResource(SyncAPIResource):
|
|
|
68
68
|
Transcribes audio into text
|
|
69
69
|
|
|
70
70
|
Args:
|
|
71
|
-
file: Audio file
|
|
71
|
+
file: Audio file upload or public HTTP/HTTPS URL. Supported formats .wav, .mp3, .m4a,
|
|
72
|
+
.webm, .flac.
|
|
72
73
|
|
|
73
74
|
diarize: Whether to enable speaker diarization. When enabled, you will get the speaker id
|
|
74
75
|
for each word in the transcription. In the response, in the words array, you
|
|
@@ -168,7 +169,7 @@ class AsyncTranscriptionsResource(AsyncAPIResource):
|
|
|
168
169
|
async def create(
|
|
169
170
|
self,
|
|
170
171
|
*,
|
|
171
|
-
file: FileTypes,
|
|
172
|
+
file: Union[FileTypes, str],
|
|
172
173
|
diarize: bool | Omit = omit,
|
|
173
174
|
language: str | Omit = omit,
|
|
174
175
|
max_speakers: int | Omit = omit,
|
|
@@ -189,7 +190,8 @@ class AsyncTranscriptionsResource(AsyncAPIResource):
|
|
|
189
190
|
Transcribes audio into text
|
|
190
191
|
|
|
191
192
|
Args:
|
|
192
|
-
file: Audio file
|
|
193
|
+
file: Audio file upload or public HTTP/HTTPS URL. Supported formats .wav, .mp3, .m4a,
|
|
194
|
+
.webm, .flac.
|
|
193
195
|
|
|
194
196
|
diarize: Whether to enable speaker diarization. When enabled, you will get the speaker id
|
|
195
197
|
for each word in the transcription. In the response, in the words array, you
|
|
@@ -47,7 +47,7 @@ class TranslationsResource(SyncAPIResource):
|
|
|
47
47
|
def create(
|
|
48
48
|
self,
|
|
49
49
|
*,
|
|
50
|
-
file: FileTypes,
|
|
50
|
+
file: Union[FileTypes, str],
|
|
51
51
|
language: str | Omit = omit,
|
|
52
52
|
model: Literal["openai/whisper-large-v3"] | Omit = omit,
|
|
53
53
|
prompt: str | Omit = omit,
|
|
@@ -65,7 +65,8 @@ class TranslationsResource(SyncAPIResource):
|
|
|
65
65
|
Translates audio into English
|
|
66
66
|
|
|
67
67
|
Args:
|
|
68
|
-
file: Audio file
|
|
68
|
+
file: Audio file upload or public HTTP/HTTPS URL. Supported formats .wav, .mp3, .m4a,
|
|
69
|
+
.webm, .flac.
|
|
69
70
|
|
|
70
71
|
language: Target output language. Optional ISO 639-1 language code. If omitted, language
|
|
71
72
|
is set to English.
|
|
@@ -145,7 +146,7 @@ class AsyncTranslationsResource(AsyncAPIResource):
|
|
|
145
146
|
async def create(
|
|
146
147
|
self,
|
|
147
148
|
*,
|
|
148
|
-
file: FileTypes,
|
|
149
|
+
file: Union[FileTypes, str],
|
|
149
150
|
language: str | Omit = omit,
|
|
150
151
|
model: Literal["openai/whisper-large-v3"] | Omit = omit,
|
|
151
152
|
prompt: str | Omit = omit,
|
|
@@ -163,7 +164,8 @@ class AsyncTranslationsResource(AsyncAPIResource):
|
|
|
163
164
|
Translates audio into English
|
|
164
165
|
|
|
165
166
|
Args:
|
|
166
|
-
file: Audio file
|
|
167
|
+
file: Audio file upload or public HTTP/HTTPS URL. Supported formats .wav, .mp3, .m4a,
|
|
168
|
+
.webm, .flac.
|
|
167
169
|
|
|
168
170
|
language: Target output language. Optional ISO 639-1 language code. If omitted, language
|
|
169
171
|
is set to English.
|