From 609c0bfe1777a21080bb10a503a4288392c25bce Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Wed, 19 Nov 2025 16:25:46 -0600 Subject: [PATCH 1/2] feat(api): Port finetuning create code from together-python --- src/together/lib/cli/api/fine_tuning.py | 22 +- src/together/lib/resources/fine_tuning.py | 149 ++++----- src/together/lib/types/fine_tuning.py | 370 +++++++++++++++++++++- src/together/resources/fine_tuning.py | 175 ++++++++++ tests/unit/test_fine_tuning_resources.py | 168 ++++++---- 5 files changed, 716 insertions(+), 168 deletions(-) diff --git a/src/together/lib/cli/api/fine_tuning.py b/src/together/lib/cli/api/fine_tuning.py index ecf9ecd0..c116d352 100644 --- a/src/together/lib/cli/api/fine_tuning.py +++ b/src/together/lib/cli/api/fine_tuning.py @@ -20,7 +20,7 @@ from together.lib.cli.api.utils import INT_WITH_MAX, BOOL_WITH_AUTO from together.lib.resources.files import DownloadManager from together.lib.utils.serializer import datetime_serializer -from together.lib.resources.fine_tuning import get_model_limits, create_fine_tuning_request +from together.lib.resources.fine_tuning import get_model_limits _CONFIRMATION_MESSAGE = ( "You are about to create a fine-tuning job. " @@ -287,13 +287,7 @@ def create( if from_checkpoint is not None: model_name = from_checkpoint.split(":")[0] - if model_name is None: - raise click.BadParameter("You must specify a model or a checkpoint") - - model_limits = get_model_limits( - client, - model=model_name, - ) + model_limits = get_model_limits(client, str(model_name)) if lora: if model_limits.lora_training is None: @@ -304,9 +298,9 @@ def create( } for arg in default_values: - arg_source = ctx.get_parameter_source("arg") + arg_source = ctx.get_parameter_source("arg") # type: ignore[attr-defined] if arg_source == ParameterSource.DEFAULT: - training_args[str(arg)] = default_values[str(arg_source)] + training_args[arg] = default_values[str(arg_source)] if ctx.get_parameter_source("lora_alpha") == ParameterSource.DEFAULT: # type: ignore[attr-defined] training_args["lora_alpha"] = training_args["lora_r"] * 2 @@ -330,18 +324,16 @@ def create( raise click.BadParameter("You have specified a number of evaluation loops but no validation file.") if confirm or click.confirm(_CONFIRMATION_MESSAGE, default=True, show_default=True): - params = create_fine_tuning_request( - model_limits=model_limits, + response = client.fine_tuning.create( **training_args, + verbose=True, ) - rprint("Submitting a fine-tuning job with the following parameters:", params) - response = client.fine_tuning.create(**params) report_string = f"Successfully submitted a fine-tuning job {response.id}" # created_at reports UTC time, we use .astimezone() to convert to local time formatted_time = response.created_at.astimezone().strftime("%m/%d/%Y, %H:%M:%S") report_string += f" at {formatted_time}" - click.echo(report_string) + rprint(report_string) else: click.echo("No confirmation received, stopping job launch") diff --git a/src/together/lib/resources/fine_tuning.py b/src/together/lib/resources/fine_tuning.py index d88de9dc..f2080118 100644 --- a/src/together/lib/resources/fine_tuning.py +++ b/src/together/lib/resources/fine_tuning.py @@ -1,27 +1,27 @@ from __future__ import annotations -from typing import Any, Dict, Literal -from typing_extensions import Union, TypeAlias +from typing import TYPE_CHECKING, Literal from rich import print as rprint -from together import Together -from together.types import ( - LrSchedulerParam, - FullTrainingTypeParam, - LoRaTrainingTypeParam, - FineTuningCreateParams, - TrainingMethodDpoParam, - TrainingMethodSftParam, - CosineLrSchedulerArgsParam, - LinearLrSchedulerArgsParam, -) from together.lib.utils import log_warn_once -from together.lib.types.fine_tuning import FinetuneTrainingLimits - -TrainingMethod: TypeAlias = Union[TrainingMethodSftParam, TrainingMethodDpoParam] -TrainingType: TypeAlias = Union[FullTrainingTypeParam, LoRaTrainingTypeParam] +if TYPE_CHECKING: + from together import Together +from together.lib.types.fine_tuning import ( + TrainingType, + FinetuneRequest, + FullTrainingType, + LoRATrainingType, + CosineLRScheduler, + LinearLRScheduler, + TrainingMethodDPO, + TrainingMethodSFT, + FinetuneLRScheduler, + CosineLRSchedulerArgs, + LinearLRSchedulerArgs, + FinetuneTrainingLimits, +) AVAILABLE_TRAINING_METHODS = { "sft", @@ -29,7 +29,7 @@ } -def create_fine_tuning_request( +def create_finetune_request( model_limits: FinetuneTrainingLimits, training_file: str, model: str | None = None, @@ -40,11 +40,11 @@ def create_fine_tuning_request( batch_size: int | Literal["max"] = "max", learning_rate: float | None = 0.00001, lr_scheduler_type: Literal["linear", "cosine"] = "cosine", - min_lr_ratio: float = 0.0, + min_lr_ratio: float | None = 0.0, scheduler_num_cycles: float = 0.5, warmup_ratio: float | None = None, max_grad_norm: float = 1.0, - weight_decay: float = 0.0, + weight_decay: float | None = 0.0, lora: bool = False, lora_r: int | None = None, lora_dropout: float | None = 0, @@ -66,7 +66,7 @@ def create_fine_tuning_request( hf_model_revision: str | None = None, hf_api_token: str | None = None, hf_output_repo_name: str | None = None, -) -> FineTuningCreateParams: +) -> FinetuneRequest: if model is not None and from_checkpoint is not None: raise ValueError("You must specify either a model or a checkpoint to start a job from, not both") @@ -87,7 +87,7 @@ def create_fine_tuning_request( if warmup_ratio is None: warmup_ratio = 0.0 - training_type: TrainingType = FullTrainingTypeParam(type="Full") + training_type: TrainingType = FullTrainingType() if lora: if model_limits.lora_training is None: raise ValueError(f"LoRA adapters are not supported for the selected model ({model_or_checkpoint}).") @@ -98,15 +98,12 @@ def create_fine_tuning_request( lora_r = lora_r if lora_r is not None else model_limits.lora_training.max_rank lora_alpha = lora_alpha if lora_alpha is not None else lora_r * 2 - training_type = LoRaTrainingTypeParam( - type="Lora", + training_type = LoRATrainingType( lora_r=lora_r, lora_alpha=int(lora_alpha), + lora_dropout=lora_dropout or 0.0, + lora_trainable_modules=lora_trainable_modules or "all-linear", ) - if lora_dropout is not None: - training_type["lora_dropout"] = lora_dropout - if lora_trainable_modules is not None: - training_type["lora_trainable_modules"] = lora_trainable_modules max_batch_size = model_limits.lora_training.max_batch_size min_batch_size = model_limits.lora_training.min_batch_size @@ -139,13 +136,13 @@ def create_fine_tuning_request( if warmup_ratio > 1 or warmup_ratio < 0: raise ValueError(f"Warmup ratio should be between 0 and 1 (got {warmup_ratio})") - if min_lr_ratio > 1 or min_lr_ratio < 0: + if min_lr_ratio is not None and (min_lr_ratio > 1 or min_lr_ratio < 0): raise ValueError(f"Min learning rate ratio should be between 0 and 1 (got {min_lr_ratio})") if max_grad_norm < 0: raise ValueError(f"Max gradient norm should be non-negative (got {max_grad_norm})") - if weight_decay < 0: + if weight_decay is not None and (weight_decay < 0): raise ValueError(f"Weight decay should be non-negative (got {weight_decay})") if training_method not in AVAILABLE_TRAINING_METHODS: @@ -154,10 +151,6 @@ def create_fine_tuning_request( if train_on_inputs is not None and training_method != "sft": raise ValueError("train_on_inputs is only supported for SFT training") - if train_on_inputs is None and training_method == "sft": - log_warn_once("train_on_inputs is not set for SFT training, it will be set to 'auto'") - train_on_inputs = "auto" - if dpo_beta is not None and training_method != "dpo": raise ValueError("dpo_beta is only supported for DPO training") if dpo_normalize_logratios_by_length and training_method != "dpo": @@ -174,24 +167,25 @@ def create_fine_tuning_request( if not simpo_gamma >= 0.0: raise ValueError(f"simpo_gamma should be non-negative (got {simpo_gamma})") - lr_scheduler: LrSchedulerParam + lr_scheduler: FinetuneLRScheduler if lr_scheduler_type == "cosine": if scheduler_num_cycles <= 0.0: raise ValueError(f"Number of cycles should be greater than 0 (got {scheduler_num_cycles})") - lr_scheduler = LrSchedulerParam( - lr_scheduler_type="cosine", - lr_scheduler_args=CosineLrSchedulerArgsParam(min_lr_ratio=min_lr_ratio, num_cycles=scheduler_num_cycles), + lr_scheduler = CosineLRScheduler( + lr_scheduler_args=CosineLRSchedulerArgs(min_lr_ratio=min_lr_ratio, num_cycles=scheduler_num_cycles), ) else: - lr_scheduler = LrSchedulerParam( - lr_scheduler_type="linear", - lr_scheduler_args=LinearLrSchedulerArgsParam(min_lr_ratio=min_lr_ratio), + lr_scheduler = LinearLRScheduler( + lr_scheduler_args=LinearLRSchedulerArgs(min_lr_ratio=min_lr_ratio), ) - training_method_cls: TrainingMethod | None = None + training_method_cls: TrainingMethodSFT | TrainingMethodDPO if training_method == "sft": - training_method_cls = TrainingMethodSftParam(method="sft", train_on_inputs=train_on_inputs or "auto") + if train_on_inputs is None: + log_warn_once("train_on_inputs is not set for SFT training, it will be set to 'auto'") + train_on_inputs = "auto" + training_method_cls = TrainingMethodSFT(train_on_inputs=train_on_inputs) elif training_method == "dpo": if simpo_gamma is not None and simpo_gamma > 0: dpo_reference_free = True @@ -204,59 +198,40 @@ def create_fine_tuning_request( else: dpo_reference_free = False - training_method_cls = TrainingMethodDpoParam( - method="dpo", + training_method_cls = TrainingMethodDPO( + dpo_beta=dpo_beta, dpo_normalize_logratios_by_length=dpo_normalize_logratios_by_length, dpo_reference_free=dpo_reference_free, + rpo_alpha=rpo_alpha, + simpo_gamma=simpo_gamma, ) - if dpo_beta is not None: - training_method_cls["dpo_beta"] = dpo_beta - if rpo_alpha is not None: - training_method_cls["rpo_alpha"] = rpo_alpha - if simpo_gamma is not None: - training_method_cls["simpo_gamma"] = simpo_gamma - - finetune_request = FineTuningCreateParams( - model=model or "", + + finetune_request = FinetuneRequest( + model=model, training_file=training_file, + validation_file=validation_file, n_epochs=n_epochs, + n_evals=n_evals, + n_checkpoints=n_checkpoints, batch_size=batch_size, + learning_rate=learning_rate or 0.00001, lr_scheduler=lr_scheduler, warmup_ratio=warmup_ratio, max_grad_norm=max_grad_norm, - weight_decay=weight_decay, + weight_decay=weight_decay or 0.0, training_type=training_type, + suffix=suffix, + wandb_key=wandb_api_key, + wandb_base_url=wandb_base_url, + wandb_project_name=wandb_project_name, + wandb_name=wandb_name, + training_method=training_method_cls, # pyright: ignore[reportPossiblyUnboundVariable] + from_checkpoint=from_checkpoint, + from_hf_model=from_hf_model, + hf_model_revision=hf_model_revision, + hf_api_token=hf_api_token, + hf_output_repo_name=hf_output_repo_name, ) - if validation_file is not None: - finetune_request["validation_file"] = validation_file - if n_evals is not None: - finetune_request["n_evals"] = n_evals - if n_checkpoints is not None: - finetune_request["n_checkpoints"] = n_checkpoints - if learning_rate is not None: - finetune_request["learning_rate"] = learning_rate - if suffix is not None: - finetune_request["suffix"] = suffix - if wandb_api_key is not None: - finetune_request["wandb_api_key"] = wandb_api_key - if wandb_base_url is not None: - finetune_request["wandb_base_url"] = wandb_base_url - if wandb_project_name is not None: - finetune_request["wandb_project_name"] = wandb_project_name - if wandb_name is not None: - finetune_request["wandb_name"] = wandb_name - if training_method_cls is not None: - finetune_request["training_method"] = training_method_cls - if from_checkpoint is not None: - finetune_request["from_checkpoint"] = from_checkpoint - if from_hf_model is not None: - finetune_request["from_hf_model"] = from_hf_model - if hf_model_revision is not None: - finetune_request["hf_model_revision"] = hf_model_revision - if hf_api_token is not None: - finetune_request["hf_api_token"] = hf_api_token - if hf_output_repo_name is not None: - finetune_request["hf_output_repo_name"] = hf_output_repo_name return finetune_request @@ -281,7 +256,3 @@ def get_model_limits(client: Together, model: str) -> FinetuneTrainingLimits: ) return response - - -def not_none_kwargs(**kwargs: Any) -> Dict[str, Any]: - return {k: v for k, v in kwargs.items() if v is not None} diff --git a/src/together/lib/types/fine_tuning.py b/src/together/lib/types/fine_tuning.py index 7c42d58b..55327e5a 100644 --- a/src/together/lib/types/fine_tuning.py +++ b/src/together/lib/types/fine_tuning.py @@ -1,8 +1,128 @@ -from typing import Any, List, Optional +from enum import Enum +from typing import Any, List, Union, Literal, Optional +from datetime import datetime +from typing_extensions import TypeAlias + +from pydantic import Field, StrictBool from ..._models import BaseModel +class FinetuneJobStatus(str, Enum): + """ + Possible fine-tune job status + """ + + STATUS_PENDING = "pending" + STATUS_QUEUED = "queued" + STATUS_RUNNING = "running" + STATUS_COMPRESSING = "compressing" + STATUS_UPLOADING = "uploading" + STATUS_CANCEL_REQUESTED = "cancel_requested" + STATUS_CANCELLED = "cancelled" + STATUS_ERROR = "error" + STATUS_USER_ERROR = "user_error" + STATUS_COMPLETED = "completed" + + +class FinetuneEventType(str, Enum): + """ + Fine-tune job event types + """ + + JOB_PENDING = "JOB_PENDING" + JOB_START = "JOB_START" + JOB_STOPPED = "JOB_STOPPED" + MODEL_DOWNLOADING = "MODEL_DOWNLOADING" + MODEL_DOWNLOAD_COMPLETE = "MODEL_DOWNLOAD_COMPLETE" + TRAINING_DATA_DOWNLOADING = "TRAINING_DATA_DOWNLOADING" + TRAINING_DATA_DOWNLOAD_COMPLETE = "TRAINING_DATA_DOWNLOAD_COMPLETE" + VALIDATION_DATA_DOWNLOADING = "VALIDATION_DATA_DOWNLOADING" + VALIDATION_DATA_DOWNLOAD_COMPLETE = "VALIDATION_DATA_DOWNLOAD_COMPLETE" + WANDB_INIT = "WANDB_INIT" + TRAINING_START = "TRAINING_START" + CHECKPOINT_SAVE = "CHECKPOINT_SAVE" + BILLING_LIMIT = "BILLING_LIMIT" + EPOCH_COMPLETE = "EPOCH_COMPLETE" + EVAL_COMPLETE = "EVAL_COMPLETE" + TRAINING_COMPLETE = "TRAINING_COMPLETE" + MODEL_COMPRESSING = "COMPRESSING_MODEL" + MODEL_COMPRESSION_COMPLETE = "MODEL_COMPRESSION_COMPLETE" + MODEL_UPLOADING = "MODEL_UPLOADING" + MODEL_UPLOAD_COMPLETE = "MODEL_UPLOAD_COMPLETE" + MODEL_UPLOADING_TO_HF = "MODEL_UPLOADING_TO_HF" + MODEL_UPLOAD_TO_HF_COMPLETE = "MODEL_UPLOAD_TO_HF_COMPLETE" + JOB_COMPLETE = "JOB_COMPLETE" + JOB_ERROR = "JOB_ERROR" + JOB_USER_ERROR = "JOB_USER_ERROR" + CANCEL_REQUESTED = "CANCEL_REQUESTED" + JOB_RESTARTED = "JOB_RESTARTED" + REFUND = "REFUND" + WARNING = "WARNING" + + +class FinetuneEventLevels(str, Enum): + """ + Fine-tune job event status levels + """ + + NULL = "" + INFO = "Info" + WARNING = "Warning" + ERROR = "Error" + LEGACY_INFO = "info" + LEGACY_IWARNING = "warning" + LEGACY_IERROR = "error" + + +class FinetuneEvent(BaseModel): + """ + Fine-tune event type + """ + + # object type + object: Literal["fine-tune-event"] + # created at datetime stamp + created_at: Union[str, None] = None + # event log level + level: Union[FinetuneEventLevels, None] = None + # event message string + message: Union[str, None] = None + # event type + type: Union[FinetuneEventType, None] = None + # optional: model parameter count + param_count: Union[int, None] = None + # optional: dataset token count + token_count: Union[int, None] = None + # optional: weights & biases url + wandb_url: Union[str, None] = None + # event hash + hash: Union[str, None] = None + + +class FullTrainingType(BaseModel): + """ + Training type for full fine-tuning + """ + + type: Union[Literal["Full"], Literal[""]] = "Full" + + +class LoRATrainingType(BaseModel): + """ + Training type for LoRA adapters training + """ + + lora_r: int + lora_alpha: int + lora_dropout: float = 0.0 + lora_trainable_modules: str = "all-linear" + type: Literal["Lora"] = "Lora" + + +TrainingType: TypeAlias = Union[FullTrainingType, LoRATrainingType] + + class FinetuneFullTrainingLimits(BaseModel): max_batch_size: int max_batch_size_dpo: int = -1 @@ -21,9 +141,257 @@ class FinetuneLoraTrainingLimits(FinetuneFullTrainingLimits): target_modules: List[str] +class TrainingMethodSFT(BaseModel): + """ + Training method type for SFT training + """ + + method: Literal["sft"] = "sft" + train_on_inputs: Union[StrictBool, Literal["auto"]] = "auto" + + +class TrainingMethodDPO(BaseModel): + """ + Training method type for DPO training + """ + + method: Literal["dpo"] = "dpo" + dpo_beta: Union[float, None] = None + dpo_normalize_logratios_by_length: bool = False + dpo_reference_free: bool = False + rpo_alpha: Union[float, None] = None + simpo_gamma: Union[float, None] = None + + +TrainingMethod: TypeAlias = Union[TrainingMethodSFT, TrainingMethodDPO] + + class FinetuneTrainingLimits(BaseModel): max_num_epochs: int max_learning_rate: float min_learning_rate: float full_training: Optional[FinetuneFullTrainingLimits] = None lora_training: Optional[FinetuneLoraTrainingLimits] = None + + +class LinearLRSchedulerArgs(BaseModel): + min_lr_ratio: Union[float, None] = 0.0 + + +class CosineLRSchedulerArgs(BaseModel): + min_lr_ratio: Union[float, None] = 0.0 + num_cycles: Union[float, None] = 0.5 + + +class LinearLRScheduler(BaseModel): + lr_scheduler_type: Literal["linear"] = "linear" + lr_scheduler_args: Union[LinearLRSchedulerArgs, None] = None + + +class CosineLRScheduler(BaseModel): + lr_scheduler_type: Literal["cosine"] = "cosine" + lr_scheduler_args: Union[CosineLRSchedulerArgs, None] = None + + +# placeholder for old fine-tuning jobs with no lr_scheduler_type specified +class EmptyLRScheduler(BaseModel): + lr_scheduler_type: Literal[""] + lr_scheduler_args: None = None + + +FinetuneLRScheduler: TypeAlias = Union[LinearLRScheduler, CosineLRScheduler, EmptyLRScheduler] + + +class FinetuneResponse(BaseModel): + """ + Fine-tune API response type + """ + + id: str + """Unique identifier for the fine-tune job""" + + created_at: datetime + """Creation timestamp of the fine-tune job""" + + status: Optional[FinetuneJobStatus] = None + """Status of the fine-tune job""" + + updated_at: datetime + """Last update timestamp of the fine-tune job""" + + batch_size: Optional[int] = None + """Batch size used for training""" + + events: Optional[List[FinetuneEvent]] = None + """Events related to this fine-tune job""" + + from_checkpoint: Optional[str] = None + """Checkpoint used to continue training""" + + from_hf_model: Optional[str] = None + """Hugging Face Hub repo to start training from""" + + hf_model_revision: Optional[str] = None + """The revision of the Hugging Face Hub model to continue training from""" + + learning_rate: Optional[float] = None + """Learning rate used for training""" + + lr_scheduler: Optional[FinetuneLRScheduler] = None + """Learning rate scheduler configuration""" + + max_grad_norm: Optional[float] = None + """Maximum gradient norm for clipping""" + + model: Optional[str] = None + """Base model used for fine-tuning""" + + output_name: Optional[str] = Field(alias="model_output_name") + """Output model name""" + + adapter_output_name: Optional[str] + """Adapter output name""" + + n_checkpoints: Optional[int] = None + """Number of checkpoints saved during training""" + + n_epochs: Optional[int] = None + """Number of training epochs""" + + n_evals: Optional[int] = None + """Number of evaluations during training""" + + owner_address: Optional[str] = None + """Owner address information""" + + suffix: Optional[str] = None + """Suffix added to the fine-tuned model name""" + + token_count: Optional[int] = None + """Count of tokens processed""" + + total_price: Optional[int] = None + """Total price for the fine-tuning job""" + + training_file: Optional[str] = None + """File-ID of the training file""" + + training_method: Optional[TrainingMethod] = None + """Method of training used""" + + training_type: Optional[TrainingType] = None + """Type of training used (full or LoRA)""" + + user_id: Optional[str] = None + """Identifier for the user who created the job""" + + validation_file: Optional[str] = None + """File-ID of the validation file""" + + wandb_name: Optional[str] = None + """Weights & Biases run name""" + + wandb_project_name: Optional[str] = None + """Weights & Biases project name""" + + wandb_base_url: Union[str, None] = None + """Weights & Biases base URL""" + + wandb_url: Union[str, None] = None + """Weights & Biases job URL""" + + warmup_ratio: Optional[float] = None + """Ratio of warmup steps""" + + weight_decay: Optional[float] = None + """Weight decay value used""" + + eval_steps: Union[int, None] = None + """number of steps between evals""" + + job_id: Optional[str] = None + """Job ID""" + + param_count: Optional[int] = None + """Model parameter count""" + + total_steps: Optional[int] = None + """Total number of training steps""" + + steps_completed: Union[int, None] = None + """Number of steps completed (incrementing counter)""" + + epochs_completed: Union[int, None] = None + """Number of epochs completed (incrementing counter)""" + + evals_completed: Union[int, None] = None + """Number of evaluation loops completed (incrementing counter)""" + + queue_depth: Union[int, None] = None + """Place in job queue (decrementing counter)""" + + # # training file metadata + training_file_num_lines: Optional[int] = Field(None, alias="TrainingFileNumLines") + training_file_size: Optional[int] = Field(None, alias="TrainingFileSize") + train_on_inputs: Union[StrictBool, Literal["auto"], None] = "auto" + + @classmethod + def validate_training_type(cls, v: TrainingType) -> TrainingType: + if v.type == "Full" or v.type == "": + return FullTrainingType(**v.model_dump()) + elif v.type == "Lora": + return LoRATrainingType(**v.model_dump()) + else: + raise ValueError("Unknown training type") + + +class FinetuneRequest(BaseModel): + """ + Fine-tune request type + """ + + # training file ID + training_file: str + # validation file id + validation_file: Union[str, None] = None + # base model string + model: Union[str, None] = None + # number of epochs to train for + n_epochs: int + # training learning rate + learning_rate: float + # learning rate scheduler type and args + lr_scheduler: Union[LinearLRScheduler, CosineLRScheduler, None] = None + # learning rate warmup ratio + warmup_ratio: float + # max gradient norm + max_grad_norm: float + # weight decay + weight_decay: float + # number of checkpoints to save + n_checkpoints: Union[int, None] = None + # number of evaluation loops to run + n_evals: Union[int, None] = None + # training batch size + batch_size: Union[int, Literal["max"], None] = None + # up to 40 character suffix for output model name + suffix: Union[str, None] = None + # weights & biases api key + wandb_key: Union[str, None] = None + # weights & biases base url + wandb_base_url: Union[str, None] = None + # wandb project name + wandb_project_name: Union[str, None] = None + # wandb run name + wandb_name: Union[str, None] = None + # training type + training_type: Union[TrainingType, None] = None + # training method + training_method: Union[TrainingMethodSFT, TrainingMethodDPO] = Field(default_factory=TrainingMethodSFT) + # from step + from_checkpoint: Union[str, None] = None + from_hf_model: Union[str, None] = None + hf_model_revision: Union[str, None] = None + # hf related fields + hf_api_token: Union[str, None] = None + hf_output_repo_name: Union[str, None] = None diff --git a/src/together/resources/fine_tuning.py b/src/together/resources/fine_tuning.py index 49e22f28..80f6b2a8 100644 --- a/src/together/resources/fine_tuning.py +++ b/src/together/resources/fine_tuning.py @@ -5,6 +5,7 @@ from typing_extensions import Literal import httpx +from rich import print as rprint from ..types import fine_tuning_delete_params, fine_tuning_download_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given @@ -27,6 +28,8 @@ ) from .._base_client import make_request_options from ..types.fine_tune import FineTune +from ..lib.types.fine_tuning import FinetuneResponse, FinetuneTrainingLimits +from ..lib.resources.fine_tuning import get_model_limits, create_finetune_request from ..types.fine_tuning_list_response import FineTuningListResponse from ..types.fine_tuning_cancel_response import FineTuningCancelResponse from ..types.fine_tuning_delete_response import FineTuningDeleteResponse @@ -56,6 +59,178 @@ def with_streaming_response(self) -> FineTuningResourceWithStreamingResponse: """ return FineTuningResourceWithStreamingResponse(self) + def create( + self, + *, + training_file: str, + model: str | None = None, + n_epochs: int = 1, + validation_file: str | None = "", + n_evals: int | None = 0, + n_checkpoints: int | None = 1, + batch_size: int | Literal["max"] = "max", + learning_rate: float | None = 0.00001, + lr_scheduler_type: Literal["linear", "cosine"] = "cosine", + min_lr_ratio: float = 0.0, + scheduler_num_cycles: float = 0.5, + warmup_ratio: float = 0.0, + max_grad_norm: float = 1.0, + weight_decay: float = 0.0, + lora: bool = True, + lora_r: int | None = None, + lora_dropout: float | None = 0, + lora_alpha: float | None = None, + lora_trainable_modules: str | None = "all-linear", + suffix: str | None = None, + wandb_api_key: str | None = None, + wandb_base_url: str | None = None, + wandb_project_name: str | None = None, + wandb_name: str | None = None, + verbose: bool = False, + model_limits: FinetuneTrainingLimits | None = None, + train_on_inputs: bool | Literal["auto"] | None = None, + training_method: str = "sft", + dpo_beta: float | None = None, + dpo_normalize_logratios_by_length: bool = False, + rpo_alpha: float | None = None, + simpo_gamma: float | None = None, + from_checkpoint: str | None = None, + from_hf_model: str | None = None, + hf_model_revision: str | None = None, + hf_api_token: str | None = None, + hf_output_repo_name: str | None = None, + ) -> FinetuneResponse: + """ + Method to initiate a fine-tuning job + + Args: + training_file (str): File-ID of a file uploaded to the Together API + model (str, optional): Name of the base model to run fine-tune job on + n_epochs (int, optional): Number of epochs for fine-tuning. Defaults to 1. + validation file (str, optional): File ID of a file uploaded to the Together API for validation. + n_evals (int, optional): Number of evaluation loops to run. Defaults to 0. + n_checkpoints (int, optional): Number of checkpoints to save during fine-tuning. + Defaults to 1. + batch_size (int or "max"): Batch size for fine-tuning. Defaults to max. + learning_rate (float, optional): Learning rate multiplier to use for training + Defaults to 0.00001. + lr_scheduler_type (Literal["linear", "cosine"]): Learning rate scheduler type. Defaults to "cosine". + min_lr_ratio (float, optional): Min learning rate ratio of the initial learning rate for + the learning rate scheduler. Defaults to 0.0. + scheduler_num_cycles (float, optional): Number or fraction of cycles for the cosine learning rate scheduler. Defaults to 0.5. + warmup_ratio (float, optional): Warmup ratio for the learning rate scheduler. + max_grad_norm (float, optional): Max gradient norm. Defaults to 1.0, set to 0 to disable. + weight_decay (float, optional): Weight decay. Defaults to 0.0. + lora (bool, optional): Whether to use LoRA adapters. Defaults to True. + lora_r (int, optional): Rank of LoRA adapters. Defaults to 8. + lora_dropout (float, optional): Dropout rate for LoRA adapters. Defaults to 0. + lora_alpha (float, optional): Alpha for LoRA adapters. Defaults to 8. + lora_trainable_modules (str, optional): Trainable modules for LoRA adapters. Defaults to "all-linear". + suffix (str, optional): Up to 40 character suffix that will be added to your fine-tuned model name. + Defaults to None. + wandb_api_key (str, optional): API key for Weights & Biases integration. + Defaults to None. + wandb_base_url (str, optional): Base URL for Weights & Biases integration. + Defaults to None. + wandb_project_name (str, optional): Project name for Weights & Biases integration. + Defaults to None. + wandb_name (str, optional): Run name for Weights & Biases integration. + Defaults to None. + verbose (bool, optional): whether to print the job parameters before submitting a request. + Defaults to False. + model_limits (FinetuneTrainingLimits, optional): Limits for the hyperparameters the model in Fine-tuning. + Defaults to None. + train_on_inputs (bool or "auto", optional): Whether to mask the user messages in conversational data or prompts in instruction data. + "auto" will automatically determine whether to mask the inputs based on the data format. + For datasets with the "text" field (general format), inputs will not be masked. + For datasets with the "messages" field (conversational format) or "prompt" and "completion" fields + (Instruction format), inputs will be masked. + Defaults to None, or "auto" if training_method is "sft" (set in create_finetune_request). + training_method (str, optional): Training method. Defaults to "sft". + Supported methods: "sft", "dpo". + dpo_beta (float, optional): DPO beta parameter. Defaults to None. + dpo_normalize_logratios_by_length (bool): Whether or not normalize logratios by sample length. Defaults to False, + rpo_alpha (float, optional): RPO alpha parameter of DPO training to include NLL in the loss. Defaults to None. + simpo_gamma: (float, optional): SimPO gamma parameter. Defaults to None. + from_checkpoint (str, optional): The checkpoint identifier to continue training from a previous fine-tuning job. + The format: {$JOB_ID/$OUTPUT_MODEL_NAME}:{$STEP}. + The step value is optional, without it the final checkpoint will be used. + from_hf_model (str, optional): The Hugging Face Hub repo to start training from. + Should be as close as possible to the base model (specified by the `model` argument) in terms of architecture and size. + hf_model_revision (str, optional): The revision of the Hugging Face Hub model to continue training from. Defaults to None. + Example: hf_model_revision=None (defaults to the latest revision in `main`) or + hf_model_revision="607a30d783dfa663caf39e06633721c8d4cfcd7e" (specific commit). + hf_api_token (str, optional): API key for the Hugging Face Hub. Defaults to None. + hf_output_repo_name (str, optional): HF repo to upload the fine-tuned model to. Defaults to None. + + Returns: + FinetuneResponse: Object containing information about fine-tuning job. + """ + + if model_limits is None: + model_name = None + # mypy doesn't understand that model or from_checkpoint is not None + if model is not None: + model_name = model + elif from_checkpoint is not None: + model_name = from_checkpoint.split(":")[0] + else: + # this branch is unreachable, but mypy doesn't know that + pass + model_limits = get_model_limits(self._client, str(model_name)) + + finetune_request = create_finetune_request( + model_limits=model_limits, + training_file=training_file, + model=model, + n_epochs=n_epochs, + validation_file=validation_file, + n_evals=n_evals, + n_checkpoints=n_checkpoints, + batch_size=batch_size, + learning_rate=learning_rate, + lr_scheduler_type=lr_scheduler_type, + min_lr_ratio=min_lr_ratio, + scheduler_num_cycles=scheduler_num_cycles, + warmup_ratio=warmup_ratio, + max_grad_norm=max_grad_norm, + weight_decay=weight_decay, + lora=lora, + lora_r=lora_r, + lora_dropout=lora_dropout, + lora_alpha=lora_alpha, + lora_trainable_modules=lora_trainable_modules, + suffix=suffix, + wandb_api_key=wandb_api_key, + wandb_base_url=wandb_base_url, + wandb_project_name=wandb_project_name, + wandb_name=wandb_name, + train_on_inputs=train_on_inputs, + training_method=training_method, + dpo_beta=dpo_beta, + dpo_normalize_logratios_by_length=dpo_normalize_logratios_by_length, + rpo_alpha=rpo_alpha, + simpo_gamma=simpo_gamma, + from_checkpoint=from_checkpoint, + from_hf_model=from_hf_model, + hf_model_revision=hf_model_revision, + hf_api_token=hf_api_token, + hf_output_repo_name=hf_output_repo_name, + ) + + if verbose: + rprint( + "Submitting a fine-tuning job with the following parameters:", + finetune_request, + ) + parameter_payload = finetune_request.model_dump(exclude_none=True) + + return self._client.post( + "/fine-tunes", + body=parameter_payload, + cast_to=FinetuneResponse, + ) + def retrieve( self, id: str, diff --git a/tests/unit/test_fine_tuning_resources.py b/tests/unit/test_fine_tuning_resources.py index 235f0afa..a8803ae4 100644 --- a/tests/unit/test_fine_tuning_resources.py +++ b/tests/unit/test_fine_tuning_resources.py @@ -1,11 +1,15 @@ +from typing import Union, Literal + import pytest from together.lib.types.fine_tuning import ( + FullTrainingType, + LoRATrainingType, FinetuneTrainingLimits, FinetuneFullTrainingLimits, FinetuneLoraTrainingLimits, ) -from together.lib.resources.fine_tuning import create_fine_tuning_request +from together.lib.resources.fine_tuning import create_finetune_request _MODEL_NAME = "meta-llama/Meta-Llama-3.1-8B-Instruct-Reference" _TRAINING_FILE = "file-7dbce5e9-7993-4520-9f3e-a7ece6c39d84" @@ -31,71 +35,88 @@ def test_simple_request(): - request = create_fine_tuning_request( + request = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, ) - assert request["model"] == _MODEL_NAME - assert request["training_file"] == _TRAINING_FILE - assert "learning_rate" in request - assert request["learning_rate"] > 0 - assert "n_epochs" in request - assert request["n_epochs"] > 0 - assert "warmup_ratio" in request - assert request["warmup_ratio"] == 0.0 - assert "training_type" in request - assert request["training_type"]["type"] == "Full" - assert "batch_size" in request - assert _MODEL_LIMITS.full_training is not None - assert request["batch_size"] == "max" + assert request.model == _MODEL_NAME + assert request.training_file == _TRAINING_FILE + assert request.learning_rate > 0 + assert request.n_epochs > 0 + assert request.warmup_ratio == 0.0 + assert request.training_type is not None + assert request.training_type.type == "Full" + assert request.batch_size == "max" def test_validation_file(): - request = create_fine_tuning_request( + request = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, validation_file=_VALIDATION_FILE, ) - assert request["training_file"] == _TRAINING_FILE - assert "validation_file" in request - assert request["validation_file"] == _VALIDATION_FILE + assert request.training_file == _TRAINING_FILE + assert request.validation_file == _VALIDATION_FILE def test_no_training_file(): with pytest.raises(TypeError, match="missing 1 required positional argument: 'training_file'"): - _ = create_fine_tuning_request( # type: ignore + _ = create_finetune_request( # type: ignore model_limits=_MODEL_LIMITS, model=_MODEL_NAME, ) def test_lora_request(): - request = create_fine_tuning_request( + request = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, lora=True, ) - assert "training_type" in request - assert request["training_type"]["type"] == "Lora" + assert isinstance(request.training_type, LoRATrainingType) + assert request.training_type.type == "Lora" assert _MODEL_LIMITS.lora_training is not None - assert request["training_type"]["lora_r"] == _MODEL_LIMITS.lora_training.max_rank - assert request["training_type"]["lora_alpha"] == _MODEL_LIMITS.lora_training.max_rank * 2 - assert "lora_dropout" in request["training_type"] - assert request["training_type"]["lora_dropout"] == 0.0 - assert "lora_trainable_modules" in request["training_type"] - assert request["training_type"]["lora_trainable_modules"] == "all-linear" - assert "batch_size" in request - assert request["batch_size"] == "max" + assert request.training_type.lora_r == _MODEL_LIMITS.lora_training.max_rank + assert request.training_type.lora_alpha == _MODEL_LIMITS.lora_training.max_rank * 2 + assert request.training_type.lora_dropout == 0.0 + assert request.training_type.lora_trainable_modules == "all-linear" + assert request.batch_size == "max" + + +@pytest.mark.parametrize("lora_dropout", [-1, 0, 0.5, 1.0, 10.0]) +def test_lora_request_with_lora_dropout(lora_dropout: float): + if 0 <= lora_dropout < 1: + request = create_finetune_request( + model_limits=_MODEL_LIMITS, + model=_MODEL_NAME, + training_file=_TRAINING_FILE, + lora=True, + lora_dropout=lora_dropout, + ) + assert isinstance(request.training_type, LoRATrainingType) + assert request.training_type.lora_dropout == lora_dropout + else: + with pytest.raises( + ValueError, + match=r"LoRA dropout must be in \[0, 1\) range.", + ): + create_finetune_request( + model_limits=_MODEL_LIMITS, + model=_MODEL_NAME, + training_file=_TRAINING_FILE, + lora=True, + lora_dropout=lora_dropout, + ) def test_dpo_request_lora(): - request = create_fine_tuning_request( + request = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -103,21 +124,18 @@ def test_dpo_request_lora(): lora=True, ) - assert "training_type" in request - assert request["training_type"]["type"] == "Lora" + assert isinstance(request.training_type, LoRATrainingType) + assert request.training_type.type == "Lora" assert _MODEL_LIMITS.lora_training is not None - assert request["training_type"]["lora_r"] == _MODEL_LIMITS.lora_training.max_rank - assert request["training_type"]["lora_alpha"] == _MODEL_LIMITS.lora_training.max_rank * 2 - assert "lora_dropout" in request["training_type"] - assert request["training_type"]["lora_dropout"] == 0.0 - assert "lora_trainable_modules" in request["training_type"] - assert request["training_type"]["lora_trainable_modules"] == "all-linear" - assert "batch_size" in request - assert request["batch_size"] == "max" + assert request.training_type.lora_r == _MODEL_LIMITS.lora_training.max_rank + assert request.training_type.lora_alpha == _MODEL_LIMITS.lora_training.max_rank * 2 + assert request.training_type.lora_dropout == 0.0 + assert request.training_type.lora_trainable_modules == "all-linear" + assert request.batch_size == "max" def test_dpo_request(): - request = create_fine_tuning_request( + request = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -125,23 +143,20 @@ def test_dpo_request(): lora=False, ) - assert "training_type" in request - assert request["training_type"]["type"] == "Full" - assert "batch_size" in request - assert _MODEL_LIMITS.full_training is not None - assert request["batch_size"] == "max" + assert isinstance(request.training_type, FullTrainingType) + assert request.training_type.type == "Full" + assert request.batch_size == "max" def test_from_checkpoint_request(): - request = create_fine_tuning_request( + request = create_finetune_request( model_limits=_MODEL_LIMITS, training_file=_TRAINING_FILE, from_checkpoint=_FROM_CHECKPOINT, ) - assert request["model"] == "" - assert "from_checkpoint" in request - assert request["from_checkpoint"] == _FROM_CHECKPOINT + assert request.model is None + assert request.from_checkpoint == _FROM_CHECKPOINT def test_both_from_checkpoint_model_name(): @@ -149,7 +164,7 @@ def test_both_from_checkpoint_model_name(): ValueError, match="You must specify either a model or a checkpoint to start a job from, not both", ): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -159,7 +174,7 @@ def test_both_from_checkpoint_model_name(): def test_no_from_checkpoint_no_model_name(): with pytest.raises(ValueError, match="You must specify either a model or a checkpoint"): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, training_file=_TRAINING_FILE, ) @@ -178,7 +193,7 @@ def test_batch_size_limit(batch_size: int, use_lora: bool): f"Requested batch size of {batch_size} is higher that the maximum allowed value of {max_batch_size}" ) with pytest.raises(ValueError, match=error_message): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -191,7 +206,7 @@ def test_batch_size_limit(batch_size: int, use_lora: bool): f"Requested batch size of {batch_size} is lower that the minimum allowed value of {min_batch_size}" ) with pytest.raises(ValueError, match=error_message): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -202,7 +217,7 @@ def test_batch_size_limit(batch_size: int, use_lora: bool): def test_non_lora_model(): with pytest.raises(ValueError, match="LoRA adapters are not supported for the selected model."): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=FinetuneTrainingLimits( max_num_epochs=20, max_learning_rate=1.0, @@ -222,7 +237,7 @@ def test_non_lora_model(): def test_non_full_model(): with pytest.raises(ValueError, match="Full training is not supported for the selected model."): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=FinetuneTrainingLimits( max_num_epochs=20, max_learning_rate=1.0, @@ -245,7 +260,7 @@ def test_non_full_model(): @pytest.mark.parametrize("warmup_ratio", [-1.0, 2.0]) def test_bad_warmup(warmup_ratio: float): with pytest.raises(ValueError, match="Warmup ratio should be between 0 and 1"): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -256,7 +271,7 @@ def test_bad_warmup(warmup_ratio: float): @pytest.mark.parametrize("min_lr_ratio", [-1.0, 2.0]) def test_bad_min_lr_ratio(min_lr_ratio: float): with pytest.raises(ValueError, match="Min learning rate ratio should be between 0 and 1"): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -267,7 +282,7 @@ def test_bad_min_lr_ratio(min_lr_ratio: float): @pytest.mark.parametrize("max_grad_norm", [-1.0, -0.01]) def test_bad_max_grad_norm(max_grad_norm: float): with pytest.raises(ValueError, match="Max gradient norm should be non-negative"): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -278,7 +293,7 @@ def test_bad_max_grad_norm(max_grad_norm: float): @pytest.mark.parametrize("weight_decay", [-1.0, -0.01]) def test_bad_weight_decay(weight_decay: float): with pytest.raises(ValueError, match="Weight decay should be non-negative"): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, @@ -288,9 +303,36 @@ def test_bad_weight_decay(weight_decay: float): def test_bad_training_method(): with pytest.raises(ValueError, match="training_method must be one of .*"): - _ = create_fine_tuning_request( + _ = create_finetune_request( model_limits=_MODEL_LIMITS, model=_MODEL_NAME, training_file=_TRAINING_FILE, training_method="NON_SFT", ) + + +@pytest.mark.parametrize("train_on_inputs", [True, False, "auto", None]) +def test_train_on_inputs_for_sft(train_on_inputs: Union[bool, Literal["auto"], None]): + request = create_finetune_request( + model_limits=_MODEL_LIMITS, + model=_MODEL_NAME, + training_file=_TRAINING_FILE, + training_method="sft", + train_on_inputs=train_on_inputs, + ) + assert request.training_method.method == "sft" + if isinstance(train_on_inputs, bool): + assert request.training_method.train_on_inputs is train_on_inputs + else: + assert request.training_method.train_on_inputs == "auto" + + +def test_train_on_inputs_not_supported_for_dpo(): + with pytest.raises(ValueError, match="train_on_inputs is only supported for SFT training"): + _ = create_finetune_request( + model_limits=_MODEL_LIMITS, + model=_MODEL_NAME, + training_file=_TRAINING_FILE, + training_method="dpo", + train_on_inputs=True, + ) From 48eb2a391cea6695bf00d4bfbab3ae88582a8afa Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Thu, 20 Nov 2025 10:40:35 -0600 Subject: [PATCH 2/2] Add Async create method --- src/together/lib/resources/fine_tuning.py | 24 ++- src/together/resources/fine_tuning.py | 174 +++++++++++++++++++++- 2 files changed, 196 insertions(+), 2 deletions(-) diff --git a/src/together/lib/resources/fine_tuning.py b/src/together/lib/resources/fine_tuning.py index f2080118..f4779191 100644 --- a/src/together/lib/resources/fine_tuning.py +++ b/src/together/lib/resources/fine_tuning.py @@ -7,7 +7,7 @@ from together.lib.utils import log_warn_once if TYPE_CHECKING: - from together import Together + from together import Together, AsyncTogether from together.lib.types.fine_tuning import ( TrainingType, FinetuneRequest, @@ -256,3 +256,25 @@ def get_model_limits(client: Together, model: str) -> FinetuneTrainingLimits: ) return response + + +async def async_get_model_limits(client: AsyncTogether, model: str) -> FinetuneTrainingLimits: + """ + Requests training limits for a specific model + + Args: + model_name (str): Name of the model to get limits for + + Returns: + FinetuneTrainingLimits: Object containing training limits for the model + """ + + response = await client.get( + "/fine-tunes/models/limits", + cast_to=FinetuneTrainingLimits, + options={ + "params": {"model_name": model}, + }, + ) + + return response diff --git a/src/together/resources/fine_tuning.py b/src/together/resources/fine_tuning.py index 80f6b2a8..4a823730 100644 --- a/src/together/resources/fine_tuning.py +++ b/src/together/resources/fine_tuning.py @@ -29,7 +29,7 @@ from .._base_client import make_request_options from ..types.fine_tune import FineTune from ..lib.types.fine_tuning import FinetuneResponse, FinetuneTrainingLimits -from ..lib.resources.fine_tuning import get_model_limits, create_finetune_request +from ..lib.resources.fine_tuning import get_model_limits, async_get_model_limits, create_finetune_request from ..types.fine_tuning_list_response import FineTuningListResponse from ..types.fine_tuning_cancel_response import FineTuningCancelResponse from ..types.fine_tuning_delete_response import FineTuningDeleteResponse @@ -500,6 +500,178 @@ def with_streaming_response(self) -> AsyncFineTuningResourceWithStreamingRespons """ return AsyncFineTuningResourceWithStreamingResponse(self) + async def create( + self, + *, + training_file: str, + model: str | None = None, + n_epochs: int = 1, + validation_file: str | None = "", + n_evals: int | None = 0, + n_checkpoints: int | None = 1, + batch_size: int | Literal["max"] = "max", + learning_rate: float | None = 0.00001, + lr_scheduler_type: Literal["linear", "cosine"] = "cosine", + min_lr_ratio: float = 0.0, + scheduler_num_cycles: float = 0.5, + warmup_ratio: float = 0.0, + max_grad_norm: float = 1.0, + weight_decay: float = 0.0, + lora: bool = True, + lora_r: int | None = None, + lora_dropout: float | None = 0, + lora_alpha: float | None = None, + lora_trainable_modules: str | None = "all-linear", + suffix: str | None = None, + wandb_api_key: str | None = None, + wandb_base_url: str | None = None, + wandb_project_name: str | None = None, + wandb_name: str | None = None, + verbose: bool = False, + model_limits: FinetuneTrainingLimits | None = None, + train_on_inputs: bool | Literal["auto"] | None = None, + training_method: str = "sft", + dpo_beta: float | None = None, + dpo_normalize_logratios_by_length: bool = False, + rpo_alpha: float | None = None, + simpo_gamma: float | None = None, + from_checkpoint: str | None = None, + from_hf_model: str | None = None, + hf_model_revision: str | None = None, + hf_api_token: str | None = None, + hf_output_repo_name: str | None = None, + ) -> FinetuneResponse: + """ + Method to initiate a fine-tuning job + + Args: + training_file (str): File-ID of a file uploaded to the Together API + model (str, optional): Name of the base model to run fine-tune job on + n_epochs (int, optional): Number of epochs for fine-tuning. Defaults to 1. + validation file (str, optional): File ID of a file uploaded to the Together API for validation. + n_evals (int, optional): Number of evaluation loops to run. Defaults to 0. + n_checkpoints (int, optional): Number of checkpoints to save during fine-tuning. + Defaults to 1. + batch_size (int or "max"): Batch size for fine-tuning. Defaults to max. + learning_rate (float, optional): Learning rate multiplier to use for training + Defaults to 0.00001. + lr_scheduler_type (Literal["linear", "cosine"]): Learning rate scheduler type. Defaults to "cosine". + min_lr_ratio (float, optional): Min learning rate ratio of the initial learning rate for + the learning rate scheduler. Defaults to 0.0. + scheduler_num_cycles (float, optional): Number or fraction of cycles for the cosine learning rate scheduler. Defaults to 0.5. + warmup_ratio (float, optional): Warmup ratio for the learning rate scheduler. + max_grad_norm (float, optional): Max gradient norm. Defaults to 1.0, set to 0 to disable. + weight_decay (float, optional): Weight decay. Defaults to 0.0. + lora (bool, optional): Whether to use LoRA adapters. Defaults to True. + lora_r (int, optional): Rank of LoRA adapters. Defaults to 8. + lora_dropout (float, optional): Dropout rate for LoRA adapters. Defaults to 0. + lora_alpha (float, optional): Alpha for LoRA adapters. Defaults to 8. + lora_trainable_modules (str, optional): Trainable modules for LoRA adapters. Defaults to "all-linear". + suffix (str, optional): Up to 40 character suffix that will be added to your fine-tuned model name. + Defaults to None. + wandb_api_key (str, optional): API key for Weights & Biases integration. + Defaults to None. + wandb_base_url (str, optional): Base URL for Weights & Biases integration. + Defaults to None. + wandb_project_name (str, optional): Project name for Weights & Biases integration. + Defaults to None. + wandb_name (str, optional): Run name for Weights & Biases integration. + Defaults to None. + verbose (bool, optional): whether to print the job parameters before submitting a request. + Defaults to False. + model_limits (FinetuneTrainingLimits, optional): Limits for the hyperparameters the model in Fine-tuning. + Defaults to None. + train_on_inputs (bool or "auto", optional): Whether to mask the user messages in conversational data or prompts in instruction data. + "auto" will automatically determine whether to mask the inputs based on the data format. + For datasets with the "text" field (general format), inputs will not be masked. + For datasets with the "messages" field (conversational format) or "prompt" and "completion" fields + (Instruction format), inputs will be masked. + Defaults to None, or "auto" if training_method is "sft" (set in create_finetune_request). + training_method (str, optional): Training method. Defaults to "sft". + Supported methods: "sft", "dpo". + dpo_beta (float, optional): DPO beta parameter. Defaults to None. + dpo_normalize_logratios_by_length (bool): Whether or not normalize logratios by sample length. Defaults to False, + rpo_alpha (float, optional): RPO alpha parameter of DPO training to include NLL in the loss. Defaults to None. + simpo_gamma: (float, optional): SimPO gamma parameter. Defaults to None. + from_checkpoint (str, optional): The checkpoint identifier to continue training from a previous fine-tuning job. + The format: {$JOB_ID/$OUTPUT_MODEL_NAME}:{$STEP}. + The step value is optional, without it the final checkpoint will be used. + from_hf_model (str, optional): The Hugging Face Hub repo to start training from. + Should be as close as possible to the base model (specified by the `model` argument) in terms of architecture and size. + hf_model_revision (str, optional): The revision of the Hugging Face Hub model to continue training from. Defaults to None. + Example: hf_model_revision=None (defaults to the latest revision in `main`) or + hf_model_revision="607a30d783dfa663caf39e06633721c8d4cfcd7e" (specific commit). + hf_api_token (str, optional): API key for the Hugging Face Hub. Defaults to None. + hf_output_repo_name (str, optional): HF repo to upload the fine-tuned model to. Defaults to None. + + Returns: + FinetuneResponse: Object containing information about fine-tuning job. + """ + + if model_limits is None: + model_name = None + # mypy doesn't understand that model or from_checkpoint is not None + if model is not None: + model_name = model + elif from_checkpoint is not None: + model_name = from_checkpoint.split(":")[0] + else: + # this branch is unreachable, but mypy doesn't know that + pass + model_limits = await async_get_model_limits(self._client, str(model_name)) + + finetune_request = create_finetune_request( + model_limits=model_limits, + training_file=training_file, + model=model, + n_epochs=n_epochs, + validation_file=validation_file, + n_evals=n_evals, + n_checkpoints=n_checkpoints, + batch_size=batch_size, + learning_rate=learning_rate, + lr_scheduler_type=lr_scheduler_type, + min_lr_ratio=min_lr_ratio, + scheduler_num_cycles=scheduler_num_cycles, + warmup_ratio=warmup_ratio, + max_grad_norm=max_grad_norm, + weight_decay=weight_decay, + lora=lora, + lora_r=lora_r, + lora_dropout=lora_dropout, + lora_alpha=lora_alpha, + lora_trainable_modules=lora_trainable_modules, + suffix=suffix, + wandb_api_key=wandb_api_key, + wandb_base_url=wandb_base_url, + wandb_project_name=wandb_project_name, + wandb_name=wandb_name, + train_on_inputs=train_on_inputs, + training_method=training_method, + dpo_beta=dpo_beta, + dpo_normalize_logratios_by_length=dpo_normalize_logratios_by_length, + rpo_alpha=rpo_alpha, + simpo_gamma=simpo_gamma, + from_checkpoint=from_checkpoint, + from_hf_model=from_hf_model, + hf_model_revision=hf_model_revision, + hf_api_token=hf_api_token, + hf_output_repo_name=hf_output_repo_name, + ) + + if verbose: + rprint( + "Submitting a fine-tuning job with the following parameters:", + finetune_request, + ) + parameter_payload = finetune_request.model_dump(exclude_none=True) + + return await self._client.post( + "/fine-tunes", + body=parameter_payload, + cast_to=FinetuneResponse, + ) + async def retrieve( self, id: str,