From f06e8cf17126ac6029625c08fc7e13639c704f1f Mon Sep 17 00:00:00 2001 From: Unsloth Date: Sat, 25 Jul 2026 19:18:35 -0700 Subject: [PATCH] Fix training start NameError, the load-order guard test and CPU-only diffusion tests - start_training forwards resume_source_run_id to _start_training_impl, which reads it. Without it every start raised NameError. - Restore main's anchor in the load-marker order test: the file now has an earlier `if config.is_gguf:`, so indexing the first one compared the wrong branch. - The two diffusion tests that reach diffusers now skip when it is absent, matching the CPU repo-test env. - The UI smoke finds nav rows that live in the sidebar's More flyout. --- studio/backend/core/training/training.py | 10 +++++++++- .../test_diffusion_dit_trainer_flow_shift.py | 3 ++- .../backend/tests/test_diffusion_te_prequant.py | 1 + .../backend/tests/test_gguf_load_cache_reuse.py | 9 +++++++-- tests/studio/playwright_chat_ui.py | 17 +++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 79469270a1..bd029ad4fc 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -864,16 +864,24 @@ class TrainingBackend: return False self._start_in_progress = True try: - return self._start_training_impl(job_id, before_spawn = before_spawn, **kwargs) + return self._start_training_impl( + job_id, + before_spawn = before_spawn, + resume_source_run_id = resume_source_run_id, + **kwargs, + ) finally: with self._lock: self._start_in_progress = False + # Named, not part of **kwargs: the body reads it directly, and it must not reach the + # worker config either (start_training's own signature keeps it out). def _start_training_impl( self, job_id: str, *, before_spawn = None, + resume_source_run_id: Optional[str] = None, **kwargs, ) -> bool: # Join prior pump thread — refuse to start if it won't die diff --git a/studio/backend/tests/test_diffusion_dit_trainer_flow_shift.py b/studio/backend/tests/test_diffusion_dit_trainer_flow_shift.py index 2fe9680798..a0339d3e5f 100644 --- a/studio/backend/tests/test_diffusion_dit_trainer_flow_shift.py +++ b/studio/backend/tests/test_diffusion_dit_trainer_flow_shift.py @@ -29,7 +29,8 @@ def _qwen_scheduler(): # The Qwen/Qwen-Image scheduler config: shift=1.0 is SKIPPED at init because # use_dynamic_shifting is true, base_shift = max_shift = log 3 (constant inference mu), # exponential time shift, terminal stretch to 0.02. - from diffusers import FlowMatchEulerDiscreteScheduler + diffusers = pytest.importorskip("diffusers") + FlowMatchEulerDiscreteScheduler = diffusers.FlowMatchEulerDiscreteScheduler return FlowMatchEulerDiscreteScheduler( num_train_timesteps = 1000, shift = 1.0, diff --git a/studio/backend/tests/test_diffusion_te_prequant.py b/studio/backend/tests/test_diffusion_te_prequant.py index 268c05de21..e7aa0c7f1a 100644 --- a/studio/backend/tests/test_diffusion_te_prequant.py +++ b/studio/backend/tests/test_diffusion_te_prequant.py @@ -550,6 +550,7 @@ def test_cast_fp8_is_idempotent_on_precast_encoder(): the engaged cast report as failed and status show no TE quant).""" import torch + pytest.importorskip("diffusers") # _cast_fp8 installs diffusers' layerwise hooks from core.inference.diffusion_precision import _cast_fp8 target = types.SimpleNamespace(dtype = torch.bfloat16) diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index dfd6fc6034..ce26147b11 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -785,7 +785,12 @@ class TestLoadHubDownloadExclusion: def test_load_marker_precedes_hub_guard_and_unload(self): source = (Path(__file__).resolve().parent.parent / "routes" / "inference.py").read_text() - gguf_branch = source[source.index("if config.is_gguf:") :] + # _load_model_impl has more than one `if config.is_gguf:`, so anchor on + # the branch that actually owns the load marker rather than the first + # one in the file, which belongs to an earlier check. + marker = source.index("enter_context(gguf_load_in_flight") + gguf_branch_start = source.rindex("if config.is_gguf:", 0, marker) + gguf_branch = source[gguf_branch_start:] # The gguf_load_in_flight marker must be entered before the hub-download # guard and the unload so a concurrent load can't race the download @@ -794,7 +799,7 @@ class TestLoadHubDownloadExclusion: # inherited value (e.g. a carried --no-mmproj) shapes the guard's # require_mmproj. Anchor on the call form so the assertion pins the # endpoint's call site, not the function definition. - assert source.index("= _resolve_inherited_extra_args(") < source.index("if config.is_gguf:") + assert source.index("= _resolve_inherited_extra_args(") < gguf_branch_start assert ( gguf_branch.index("enter_context(gguf_load_in_flight") < gguf_branch.index("_hub_download_blocks_gguf_load") diff --git a/tests/studio/playwright_chat_ui.py b/tests/studio/playwright_chat_ui.py index a06e559100..bf2ffcd840 100644 --- a/tests/studio/playwright_chat_ui.py +++ b/tests/studio/playwright_chat_ui.py @@ -1155,6 +1155,23 @@ with sync_playwright() as p: if c.count() > 0: btn = c break + if btn is None: + # Unpinned rows (Video, Recipes, Export by default) live in the sidebar's + # "More" flyout. It opens on hover, so hover first; a click would toggle it + # straight back shut. Click is the fallback for a no-hover environment. + more_btn = page.get_by_role( + "button", name = re.compile(r"^\s*More\s*$", re.I) + ).first + if more_btn.count() > 0: + more_btn.hover() + page.wait_for_timeout(500) + item = page.get_by_role("menuitem", name = re.compile(label, re.I)).first + if item.count() == 0: + more_btn.click(force = True) + page.wait_for_timeout(500) + item = page.get_by_role("menuitem", name = re.compile(label, re.I)).first + if item.count() > 0: + btn = item if btn is None: soft_fail(f"nav '{label}' not found") return False