Studio vision fine-tuning had no explicit way to cap image resolution, so
users could not trade visual detail against context and memory use from the
training UI, YAML config, or API payload. :) Add a nullable `vision_image_size`
setting that keeps the current model default when unset and applies a
max-side resize when provided.
- Add `vision_image_size` to the training request model, route payload, backend
training config, and frontend API/types plumbing.
- Validate the value server-side as either null or an integer in the supported
256-2048 range.
- Surface an Image Size selector for vision LoRA training with Default plus
common preset sizes.
- Include the value in training start payloads only for image-dataset vision
models, and serialize it into vision-aware YAML configs.
- Map backend model defaults back into the training store and reset the value
when reapplying model defaults.
- Pass the resize through the Torch trainer via `UnslothVisionDataCollator`
using max-dimension semantics.
- Apply the same max-dimension resize in the MLX VLM path before mlx-vlm's
internal collation, preserving aspect ratio and avoiding upscaling.
- Add backend validation coverage and MLX resize-size tests for the new
behavior.
114 lines
3.4 KiB
Python
114 lines
3.4 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
"""Pin TrainingStartRequest hyperparameter caps at the at-cap / over-cap boundary."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(_BACKEND_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_BACKEND_ROOT))
|
|
|
|
from models.training import (
|
|
_MAX_BATCH_SIZE,
|
|
_MAX_LORA_ALPHA,
|
|
_MAX_LORA_R,
|
|
_MAX_SEQ_LENGTH,
|
|
_MAX_VISION_IMAGE_SIZE,
|
|
_MIN_VISION_IMAGE_SIZE,
|
|
)
|
|
|
|
|
|
def _check_field(field_name: str, value):
|
|
"""Run the field validator without constructing a full TrainingStartRequest."""
|
|
from models.training import TrainingStartRequest
|
|
|
|
schema_field = TrainingStartRequest.model_fields[field_name]
|
|
return TrainingStartRequest.__pydantic_validator__.validate_assignment(
|
|
TrainingStartRequest.model_construct(),
|
|
field_name,
|
|
value,
|
|
)
|
|
|
|
|
|
class TestSeqLengthCap:
|
|
def test_at_cap_accepts(self):
|
|
_check_field("max_seq_length", _MAX_SEQ_LENGTH)
|
|
assert _MAX_SEQ_LENGTH == 2_000_000
|
|
|
|
def test_over_cap_rejects(self):
|
|
with pytest.raises(ValidationError) as exc:
|
|
_check_field("max_seq_length", _MAX_SEQ_LENGTH + 1)
|
|
assert "max_seq_length" in str(exc.value)
|
|
|
|
def test_below_min_rejects(self):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("max_seq_length", 0)
|
|
|
|
|
|
class TestBatchSizeCap:
|
|
def test_at_cap_accepts(self):
|
|
_check_field("batch_size", _MAX_BATCH_SIZE)
|
|
assert _MAX_BATCH_SIZE == 4096
|
|
|
|
def test_over_cap_rejects(self):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("batch_size", _MAX_BATCH_SIZE + 1)
|
|
|
|
def test_below_min_rejects(self):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("batch_size", 0)
|
|
|
|
|
|
class TestVisionImageSizeCap:
|
|
def test_none_accepts_model_default(self):
|
|
_check_field("vision_image_size", None)
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[_MIN_VISION_IMAGE_SIZE, 640, 1000, _MAX_VISION_IMAGE_SIZE],
|
|
)
|
|
def test_in_range_accepts(self, value):
|
|
_check_field("vision_image_size", value)
|
|
assert _MIN_VISION_IMAGE_SIZE == 256
|
|
assert _MAX_VISION_IMAGE_SIZE == 2048
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[_MIN_VISION_IMAGE_SIZE - 1, _MAX_VISION_IMAGE_SIZE + 1, 640.5, True],
|
|
)
|
|
def test_invalid_rejects(self, value):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("vision_image_size", value)
|
|
|
|
|
|
class TestLoraRCap:
|
|
def test_at_cap_accepts(self):
|
|
_check_field("lora_r", _MAX_LORA_R)
|
|
assert _MAX_LORA_R == 16_384
|
|
|
|
def test_over_cap_rejects(self):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("lora_r", _MAX_LORA_R + 1)
|
|
|
|
def test_below_min_rejects(self):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("lora_r", 0)
|
|
|
|
|
|
class TestLoraAlphaCap:
|
|
def test_at_cap_accepts(self):
|
|
_check_field("lora_alpha", _MAX_LORA_ALPHA)
|
|
assert _MAX_LORA_ALPHA == 32_768
|
|
|
|
def test_over_cap_rejects(self):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("lora_alpha", _MAX_LORA_ALPHA + 1)
|
|
|
|
def test_below_min_rejects(self):
|
|
with pytest.raises(ValidationError):
|
|
_check_field("lora_alpha", 0)
|