-
-
Notifications
You must be signed in to change notification settings - Fork 7.1k
studio: show the active run's saved config in the Training Progress popover #7217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
danielhanchen
merged 9 commits into
unslothai:main
from
hakanbaysal:fix/studio-training-config-popover
Jul 19, 2026
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
3e96943
studio: show the active run's saved config in the Training Progress p…
hakanbaysal 20a877c
studio: retry the run-config fetch after the first step, carry the sa…
hakanbaysal 40a3187
studio: fetch the run config on a terminal phase too, not just the fi…
hakanbaysal c10eed5
Merge branch 'main' into fix/studio-training-config-popover
hakanbaysal bc495a2
studio: retry the run-config lookup and accept a hydrated step as row…
hakanbaysal 3307b5f
Merge branch 'main' into fix/studio-training-config-popover
hakanbaysal 0c56ca3
studio: fetch the saved run config as soon as the job id exists
hakanbaysal 893a612
Fix Training Config popover fallback for history runs without a saved…
shimmyshimmer 07b6aba
Merge branch 'main' into fix/studio-training-config-popover
hakanbaysal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
109 changes: 109 additions & 0 deletions
109
studio/backend/tests/test_training_config_popover_source.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 | ||
|
|
||
| """Source-level regression guards for the Training Config popover data source | ||
| (#6853). | ||
|
|
||
| The live Training Progress popover used to read the editable form store | ||
| (useTrainingConfigStore) while a run was active, so it showed stale/static | ||
| values whenever the user touched the form after starting the run; only the | ||
| History view read the run's saved config snapshot. These guards pin the fixed | ||
| wiring: both views feed ProgressSection a config override mapped from | ||
| GET /api/train/runs/{id}, and ProgressSection prefers that override whenever | ||
| one is present -- not only for historical views. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| _STUDIO_FRONTEND = Path(__file__).resolve().parents[2] / "frontend" / "src" / "features" / "studio" | ||
|
|
||
|
|
||
| def _read(rel: str) -> str: | ||
| return (_STUDIO_FRONTEND / rel).read_text(encoding = "utf-8") | ||
|
|
||
|
|
||
| def test_progress_section_prefers_override_over_form_store(): | ||
| src = _read("sections/progress-section.tsx") | ||
| # Fields key on the override's presence, not isHistorical: a live view passing | ||
| # an override wins over the store; without one, live keeps the store while | ||
| # History shows blanks rather than unrelated live form values. | ||
| assert "const cfg = configOverride ?? (isHistorical ? undefined : config)" in src | ||
| assert "const cfgEpochs = cfg?.epochs" in src | ||
| assert "isHistorical ? configOverride?.epochs" not in src | ||
|
|
||
|
|
||
| def test_live_view_fetches_the_active_run_config(): | ||
| src = _read("live-training-view.tsx") | ||
| # Live view resolves the run's saved config snapshot by job id... | ||
| assert "getTrainingRun(" in src | ||
| assert "mapRunConfigToOverride(" in src | ||
| # ...and hands it to the popover. | ||
| assert "configOverride={runConfigOverride}" in src | ||
|
|
||
|
|
||
| def test_live_view_fetches_as_soon_as_the_job_id_exists(): | ||
| # start_training() inserts the run row BEFORE the pump consumes any event, so | ||
| # the saved config is available during configuring/loading/downloading. The | ||
| # job id is therefore the whole readiness condition: gating on a first step | ||
| # or a terminal phase would show the wrong config for the entire pre-step | ||
| # window of a long load, or for a run adopted from another client. | ||
| src = _read("live-training-view.tsx") | ||
| assert "if (!runtime.jobId) {" in src | ||
| assert "[runtime.jobId, fetchedRunConfig, fetchAttempt]" in src | ||
| # No step/phase readiness gate may creep back in. | ||
| assert "runRowReady" not in src | ||
|
|
||
|
|
||
| def test_live_view_retries_the_transient_row_miss(): | ||
| # start_training() creates the row before the pump, but a lookup racing that | ||
| # commit can still 404. Nothing else in the effect deps changes on failure, so | ||
| # the retry must be explicit and bounded, else a genuinely absent row would | ||
| # poll forever instead of falling back to the form store. | ||
| src = _read("live-training-view.tsx") | ||
| assert "RUN_CONFIG_FETCH_RETRIES" in src | ||
| assert "RUN_CONFIG_FETCH_RETRY_MS" in src | ||
| assert "setFetchAttempt(" in src | ||
| assert "attempts >= RUN_CONFIG_FETCH_RETRIES" in src | ||
| # The budget is keyed by job so a new run always starts fresh. | ||
| assert "fetchAttempt?.jobId === jobId ? fetchAttempt.count : 0" in src | ||
| # The pending retry must be cancelled with the effect. | ||
| assert "clearTimeout(retryTimer)" in src | ||
|
|
||
|
|
||
| def test_live_view_prefers_saved_training_method(): | ||
| # The method label / LoRA-row visibility must come from the run snapshot, | ||
| # not the editable form (which may have changed since the run started). | ||
| src = _read("live-training-view.tsx") | ||
| assert "runConfigOverride?.trainingMethod ?? config.trainingMethod" in src | ||
|
|
||
|
|
||
| def test_history_view_uses_the_shared_mapper(): | ||
| src = _read("historical-training-view.tsx") | ||
| # Shared mapper, not a re-inlined field-by-field copy that could drift. | ||
| assert "mapRunConfigToOverride(detail.config)" in src | ||
| assert "num_epochs" not in src | ||
|
|
||
|
|
||
| def test_shared_mapper_matches_backend_config_keys(): | ||
| src = _read("sections/run-config-override.ts") | ||
| # The mapper reads the run config JSON the backend snapshots at job start; | ||
| # keep the key set pinned so a silent rename breaks loudly here. | ||
| for key in ( | ||
| "training_type", | ||
| "load_in_4bit", | ||
| "num_epochs", | ||
| "batch_size", | ||
| "learning_rate", | ||
| "max_steps", | ||
| "max_seq_length", | ||
| "warmup_steps", | ||
| "optim", | ||
| "lora_r", | ||
| "lora_alpha", | ||
| "lora_dropout", | ||
| "use_rslora", | ||
| "use_loftq", | ||
| ): | ||
| assert key in src, f"run-config mapper lost backend key {key}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
studio/frontend/src/features/studio/sections/run-config-override.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 | ||
|
|
||
| import { parseBackendTrainingMethod } from "@/features/training"; | ||
|
|
||
| /** Shape of the Training Config popover's data when it is driven by a saved | ||
| * run snapshot instead of the editable form store. */ | ||
| export interface RunConfigOverride { | ||
| trainingMethod?: string; | ||
| epochs?: number; | ||
| batchSize?: number; | ||
| learningRate?: string; | ||
| maxSteps?: number; | ||
| contextLength?: number; | ||
| warmupSteps?: number; | ||
| optimizerType?: string; | ||
| loraRank?: number; | ||
| loraAlpha?: number; | ||
| loraDropout?: number; | ||
| loraVariant?: string; | ||
| } | ||
|
|
||
| /** Map a saved run's config (GET /api/train/runs/{id} `detail.config`) into the | ||
| * Training Config popover's override shape. Shared by the History view and the | ||
| * live Current Run view so both read the same authoritative run snapshot | ||
| * instead of the editable form store (#6853). */ | ||
| export function mapRunConfigToOverride( | ||
| config: Record<string, unknown> | null | undefined, | ||
| ): RunConfigOverride | undefined { | ||
| if (!config) { | ||
| return undefined; | ||
| } | ||
| return { | ||
| trainingMethod: parseBackendTrainingMethod( | ||
| config.training_type, | ||
| config.load_in_4bit, | ||
| ), | ||
| epochs: config.num_epochs as number | undefined, | ||
| batchSize: config.batch_size as number | undefined, | ||
| learningRate: config.learning_rate as string | undefined, | ||
| maxSteps: config.max_steps as number | undefined, | ||
| contextLength: config.max_seq_length as number | undefined, | ||
| warmupSteps: config.warmup_steps as number | undefined, | ||
| optimizerType: config.optim as string | undefined, | ||
| loraRank: config.lora_r as number | undefined, | ||
| loraAlpha: config.lora_alpha as number | undefined, | ||
| loraDropout: config.lora_dropout as number | undefined, | ||
| loraVariant: config.use_rslora | ||
| ? "rslora" | ||
| : config.use_loftq | ||
| ? "loftq" | ||
| : "lora", | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.