diff --git a/src/together/lib/cli/__init__.py b/src/together/lib/cli/__init__.py index 6bf64a49..248a56ec 100644 --- a/src/together/lib/cli/__init__.py +++ b/src/together/lib/cli/__init__.py @@ -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)) diff --git a/src/together/lib/cli/api/fine_tuning/model_limits.py b/src/together/lib/cli/api/fine_tuning/model_limits.py new file mode 100644 index 00000000..a5d77bf2 --- /dev/null +++ b/src/together/lib/cli/api/fine_tuning/model_limits.py @@ -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) diff --git a/src/together/lib/resources/fine_tuning.py b/src/together/lib/resources/fine_tuning.py index 3aada787..b6a1cf22 100644 --- a/src/together/lib/resources/fine_tuning.py +++ b/src/together/lib/resources/fine_tuning.py @@ -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 @@ -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: @@ -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 diff --git a/tests/cli/test_fine_tuning.py b/tests/cli/test_fine_tuning.py index ad31400b..1d599125 100644 --- a/tests/cli/test_fine_tuning.py +++ b/tests/cli/test_fine_tuning.py @@ -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 = { @@ -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(