Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
73f37c6
Expose MLX grad value clipping in Studio
mmathew23 May 19, 2026
e36b55e
update test
mmathew23 May 20, 2026
8b79ba4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 20, 2026
e8c944f
dataset ordering + wd
mmathew23 May 21, 2026
377fc67
fix mlx smoke step expectations
mmathew23 May 21, 2026
e829268
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 21, 2026
bfb4203
cast norm activation output back to original input dtype
mmathew23 May 21, 2026
a404dfd
address mlx studio review feedback
mmathew23 May 21, 2026
bff5b44
Fix present-but-None seed override for PR #5656
May 24, 2026
56e32b7
Guard optional MLXTrainingConfig fields and normalize random_seed for…
May 24, 2026
29aa91a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
1a02643
Normalize seed / cast / max_grad_value at TrainingBackend for PR #5656
May 24, 2026
e293af1
Tighten feature-detect test paren tracking for PR #5656
May 24, 2026
962ca28
Shorten verbose comments in MLX Studio backend
May 25, 2026
65cd019
Handle MLX Studio EOS appending by mode
mmathew23 May 26, 2026
d66f4a7
Wire MLX leaf norm clipping through Studio
mmathew23 May 26, 2026
6a406cb
Respect VLM layer filters for explicit LoRA targets
mmathew23 May 26, 2026
ad8bf14
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
976520c
Refresh MLX smoke clip-config note for leaf_norm default
May 27, 2026
32ddc22
Merge main into explore/mlx; resolve studio test + smoke conflicts
May 27, 2026
ae6c259
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
54e8408
Merge main into explore/mlx and resolve smoke, worker, and vision tar…
danielhanchen Jun 12, 2026
71c363d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 12, 2026
d142420
Forward max_grad_leaf_norm through the training route and warn when l…
danielhanchen Jun 12, 2026
54d8d15
Merge remote-tracking branch 'origin/main' into explore/mlx
danielhanchen Jun 13, 2026
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
1 change: 1 addition & 0 deletions studio/backend/core/training/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ def start_training(self, job_id: str, **kwargs) -> bool:
"save_steps": kwargs.get("save_steps", 0),
"weight_decay": kwargs.get("weight_decay", 0.001),
"max_grad_norm": kwargs.get("max_grad_norm", 0.0),
"max_grad_value": kwargs.get("max_grad_value"),
"random_seed": kwargs.get("random_seed", 3407),
"packing": kwargs.get("packing", False),
"optim": kwargs.get("optim", "adamw_8bit"),
Expand Down
9 changes: 6 additions & 3 deletions studio/backend/core/training/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,14 +1156,16 @@ def _send(event_type, **kwargs):
is_dataset_image = bool(config.get("is_dataset_image", False))
training_type = config.get("training_type", "LoRA/QLoRA")
use_lora = training_type == "LoRA/QLoRA"
model_random_state = config.get("model_random_state", 3407)
lora_random_state = config.get("lora_random_state", 3407)
Comment thread
Imagineer99 marked this conversation as resolved.
Outdated
Comment thread
Imagineer99 marked this conversation as resolved.
Outdated
model, tokenizer = FastMLXModel.from_pretrained(
model_name,
load_in_4bit = config.get("load_in_4bit", True),
full_finetuning = not use_lora,
text_only = None if is_dataset_image else True,
token = hf_token,
trust_remote_code = bool(config.get("trust_remote_code", False)),
random_state = config.get("random_seed", 3407),
random_state = model_random_state,
)

is_vlm = bool(is_dataset_image and getattr(model, "_is_vlm_model", False))
Expand All @@ -1188,7 +1190,7 @@ def _send(event_type, **kwargs):
lora_dropout = config.get("lora_dropout", 0.0),
use_rslora = config.get("use_rslora", False),
init_lora_weights = config.get("init_lora_weights", True),
random_state = config.get("random_seed", 3407),
random_state = lora_random_state,
target_modules = config.get("target_modules")
or [
"q_proj",
Expand Down Expand Up @@ -1390,7 +1392,8 @@ def _fmt_progress(status_message = "", **_kw):
# global reduction that breaks MLX's eager pipeline). 1.0 (not 5.0):
# |g_i| > 5 rarely fires, so the historical 5.0 was effectively no-op.
max_grad_norm = 0.0
max_grad_value = 1.0 # TODO: expose MLX grad-clip in Studio UI for power users
max_grad_value = config.get("max_grad_value")
max_grad_value = 1.0 if max_grad_value is None else float(max_grad_value)
Comment thread
Imagineer99 marked this conversation as resolved.
Outdated
Comment thread
Imagineer99 marked this conversation as resolved.
Outdated

trainer = MLXTrainer(
model = model,
Expand Down
8 changes: 8 additions & 0 deletions studio/backend/models/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ def _check_lora_dropout(cls, v: float) -> float:
ge = 0,
description = "Global gradient norm clipping threshold. Set 0 to disable.",
)
max_grad_value: Optional[float] = Field(
None,
ge = 0,
description = (
"MLX-only elementwise gradient value clipping threshold. "
"If unset, MLX uses its runtime default."
),
)
Comment on lines +328 to +335

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for max_grad_value states that MLX uses its runtime default if unset. However, the implementation in worker.py (line 1396) explicitly defaults it to 1.0 if it is None. To avoid confusion and ensure the API documentation matches the implementation, the description should be updated to reflect that it defaults to 1.0 in this environment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is resolved in the current head. The worker no longer substitutes 1.0: max_grad_value stays None unless the caller sets it, and None reaches MLXTrainingConfig so the trainer applies its own runtime default (per-leaf L2 norm 1.0 after unslothai/unsloth-zoo#684). The schema description now matches the implementation.

random_seed: int = Field(42, description = "Random seed")
packing: bool = Field(False, description = "Enable sequence packing")
optim: str = Field("adamw_8bit", description = "Optimizer")
Expand Down
1 change: 1 addition & 0 deletions studio/backend/routes/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ async def start_training(
"save_steps": request.save_steps,
"weight_decay": request.weight_decay,
"max_grad_norm": request.max_grad_norm,
"max_grad_value": request.max_grad_value,
"random_seed": request.random_seed,
"packing": request.packing,
"optim": request.optim,
Expand Down
9 changes: 9 additions & 0 deletions studio/backend/tests/test_mlx_training_worker_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,12 @@ def test_mlx_studio_rejects_unknown_optimizer():
def test_mlx_studio_rejects_unknown_scheduler():
with pytest.raises(ValueError, match = "Unsupported LR scheduler for MLX training"):
_normalize_mlx_studio_scheduler("linear_typo")


def test_mlx_studio_keeps_hf_style_tokenizer_dual_purpose():
source = (
Path(__file__).resolve().parents[1] / "core" / "training" / "worker.py"
).read_text()

assert "tokenizer = tokenizer" in source
assert "processor = tokenizer if is_vlm else None" not in source
15 changes: 15 additions & 0 deletions studio/backend/tests/test_training_raw_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,25 @@ def start(self):
model_name = "unsloth/test",
training_type = "LoRA/QLoRA",
max_grad_norm = 0.7,
max_grad_value = 3.0,
)

config = mock_process.call_args.kwargs["kwargs"]["config"]
self.assertEqual(config["max_grad_norm"], 0.7)
self.assertEqual(config["max_grad_value"], 3.0)

def test_mlx_worker_uses_cuda_style_model_and_lora_init_seed(self):
source = (_BACKEND_ROOT / "core" / "training" / "worker.py").read_text()

self.assertIn(
'model_random_state = config.get("model_random_state", 3407)', source
)
self.assertIn(
'lora_random_state = config.get("lora_random_state", 3407)', source
)
Comment thread
mmathew23 marked this conversation as resolved.
self.assertIn("random_state = model_random_state", source)
self.assertIn("random_state = lora_random_state", source)
self.assertIn('seed = config.get("random_seed", 3407)', source)

def test_training_route_forwards_embedding_learning_rate(self):
training_route = _load_route_module(
Expand Down
1 change: 1 addition & 0 deletions studio/frontend/src/features/training/api/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function buildTrainingStartPayload(
eval_steps: config.evalSteps,
weight_decay: config.weightDecay,
max_grad_norm: 0.0,
max_grad_value: null,
random_seed: config.randomSeed,
packing: isEmbedding ? false : config.packing,
optim: config.optimizerType,
Expand Down
1 change: 1 addition & 0 deletions studio/frontend/src/features/training/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface TrainingStartRequest {
eval_steps: number;
weight_decay: number;
max_grad_norm: number;
max_grad_value?: number | null;
random_seed: number;
packing: boolean;
optim: string;
Expand Down
Loading