Skip to content
Draft
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
4 changes: 4 additions & 0 deletions src/together/lib/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ async def run_command() -> None:
help="Retrieve training metrics for a fine-tuning job",
help_epilogue=FINE_TUNING_LIST_METRICS_HELP_EXAMPLES,
)
fine_tuning_app.command(
(f"{_CLI}.fine_tuning.model_limits:model_limits"),
help="Get fine-tuning limits for a model",
)

## Models API commands
models_app = app.command(App(name="models", help="List and upload models", help_epilogue=MODELS_HELP_EXAMPLES))
Expand Down
29 changes: 29 additions & 0 deletions src/together/lib/cli/api/fine_tuning/model_limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

from typing import Annotated

from cyclopts import Parameter

from together._utils._json import openapi_dumps
from together.lib.cli.utils.config import CLIConfigParameter
from together.lib.cli.utils._console import console
from together.lib.cli.components.loader import show_loading_status
from together.lib.cli.components.model_dump import print_model_dump


async def model_limits(
model_name: Annotated[str, Parameter(help="The model name to get fine-tuning limits for")],
*,
config: CLIConfigParameter,
) -> None:
"""Get fine-tuning limits for a model."""
response = await show_loading_status(
"Fetching model limits...",
config.client.fine_tuning.model_limits(model_name=model_name),
)

if config.json:
console.print_json(openapi_dumps(response).decode("utf-8"))
return

print_model_dump(response, show_nulls=False)
35 changes: 18 additions & 17 deletions src/together/lib/resources/fine_tuning.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from together.types import fine_tuning_estimate_price_params as pe_params
from together.lib.utils import log_warn_once
from together.types.fine_tuning_model_limits_response import FineTuningModelLimitsResponse

if TYPE_CHECKING:
from together import Together, AsyncTogether
Expand Down Expand Up @@ -389,15 +390,8 @@ def get_model_limits(client: Together, model: str) -> FinetuneTrainingLimits:
FinetuneTrainingLimits: Object containing training limits for the model
"""

response = client.get(
"/fine-tunes/models/limits",
cast_to=FinetuneTrainingLimits,
options={
"params": {"model_name": model},
},
)

return response
response = client.fine_tuning.model_limits(model_name=model)
return _model_limits_response_to_training_limits(response)


async def async_get_model_limits(client: AsyncTogether, model: str) -> FinetuneTrainingLimits:
Expand All @@ -411,12 +405,19 @@ async def async_get_model_limits(client: AsyncTogether, model: str) -> FinetuneT
FinetuneTrainingLimits: Object containing training limits for the model
"""

response = await client.get(
"/fine-tunes/models/limits",
cast_to=FinetuneTrainingLimits,
options={
"params": {"model_name": model},
},
response = await client.fine_tuning.model_limits(model_name=model)
return _model_limits_response_to_training_limits(response)


def _model_limits_response_to_training_limits(response: FineTuningModelLimitsResponse) -> FinetuneTrainingLimits:
return FinetuneTrainingLimits(
max_num_epochs=response.max_num_epochs,
max_learning_rate=response.max_learning_rate,
min_learning_rate=response.min_learning_rate,
min_max_seq_length=response.min_max_seq_length,
max_seq_length_sft=response.max_seq_length_sft,
max_seq_length_dpo=response.max_seq_length_dpo,
full_training=response.full_training.to_dict() if response.full_training is not None else None,
lora_training=response.lora_training.to_dict(),
supports_vision=response.supports_vision,
)

return response
48 changes: 48 additions & 0 deletions tests/cli/test_fine_tuning.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,33 @@
}

_MODEL_LIMITS_BODY = {
"model_name": "meta-llama/Llama-3-8b",
"default_gradient_accumulation_steps": 1,
"max_num_epochs": 10,
"max_num_checkpoints": 5,
"max_num_evals": 20,
"max_learning_rate": 1,
"min_learning_rate": 0,
"min_max_seq_length": 1,
"max_seq_length_sft": 4096,
"max_seq_length_dpo": 4096,
"merge_output_lora": True,
"supports_full_training": True,
"supports_reasoning": False,
"supports_tools": True,
"supports_vision": False,
"full_training": {
"max_batch_size": 64,
"max_batch_size_dpo": 32,
"min_batch_size": 1,
},
"lora_training": {
"max_batch_size": 128,
"max_batch_size_dpo": 64,
"max_rank": 64,
"min_batch_size": 1,
"target_modules": ["q_proj", "v_proj"],
},
}

_FT_CREATE_BODY = {
Expand Down Expand Up @@ -397,6 +418,33 @@ def test_list_metrics_json_includes_zero_step_filters(self, respx_mock: MockRout
assert json.loads(result.output) == _FT_METRICS_BODY["metrics"]


class TestFineTuningModelLimits:
@pytest.mark.respx(base_url=base_url)
def test_model_limits_json(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None:
route = respx_mock.get("/fine-tunes/models/limits").mock(
return_value=httpx.Response(200, json=_MODEL_LIMITS_BODY)
)

result = cli_runner.invoke(["fine-tuning", "model-limits", "meta-llama/Llama-3-8b", "--json"])

assert result.exit_code == 0
params = cast(Call, route.calls[0]).request.url.params
assert params["model_name"] == "meta-llama/Llama-3-8b"
body = json.loads(result.output)
assert body["model_name"] == "meta-llama/Llama-3-8b"
assert body["lora_training"]["max_rank"] == 64

@pytest.mark.respx(base_url=base_url)
def test_model_limits_ft_alias_table(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None:
respx_mock.get("/fine-tunes/models/limits").mock(return_value=httpx.Response(200, json=_MODEL_LIMITS_BODY))

result = cli_runner.invoke(["ft", "model-limits", "meta-llama/Llama-3-8b"])

assert result.exit_code == 0
assert "meta-llama/Llama-3-8b" in result.output
assert "Max Rank" in result.output


class TestFineTuningDownload:
@pytest.mark.respx(base_url=base_url)
def test_download_invokes_download_manager(
Expand Down