mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-24 00:04:14 +00:00
2 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7103733d7b
|
Studio: hold the Mac capability verdict while an MLX repair can still overturn it (#8152)
* Studio: keep the Mac capability verdict provisional while MLX repair runs * Studio: bound the MLX repair hold so a scheduler that never runs cannot spin the tabs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: gate the Mac capability verdict on a real Apple Silicon host The hold that keeps /api/health from publishing a chat-only verdict an MLX self-heal is about to overturn was covered only by unit tests, which drive it with a fake clock and a stand-in repair worker. Nothing ran it where `import mlx.core` succeeds or fails for real. tests/studio/mac_capability_verdict_smoke.py boots the real `unsloth studio` through .github/scripts/boot-studio-api-only.sh and polls /api/health from the first reply the socket gives, judging the whole sequence rather than the last answer, since the reported bug was a verdict that was wrong for a while and right afterwards. Three boots, one per scenario, because the self-heal is once per process: - real-mlx: the verdict must settle chat_only=false with /api/system reporting device_backend "mlx", and no reply on the way there may publish chat_only true. - no-mlx-settles: MLX made unimportable and UNSLOTH_DISABLE_MLX_AUTOREPAIR=1. The verdict must still settle promptly to mlx_unavailable, or a Mac with no repair coming spins Train and Video for the session. - no-mlx-repair: MLX made unimportable, self-heal on. No reply may publish the verdict while the installer runs, including well past the pre-start grace so the hold is provably the live worker, and it must settle once the install fails. MLX is made unimportable the way tests/studio/test_hardware_dispatch_matrix.py does it, through the import system, but from a sitecustomize on PYTHONPATH since there is a server to boot. It hides mlx from PathFinder rather than raising, so the find_spec in unsloth/__init__.py answers None instead of crashing. The installer in the third scenario is a stub uv first on PATH that records its argv and takes its time; utils.mlx_repair is untouched and runs its real thread, real command and real subprocess against it. The recorded argv is asserted, so a run where the self-heal never reached the installer fails rather than passing quietly. mlx-ci.yml gains the three steps plus a venv at $STUDIO_HOME/unsloth_studio, which is what `unsloth studio` re-execs into and refuses to start without. Building it over the job's own site-packages gets the real launch path without the ten minutes install.sh costs. Path filters pick up main.py, mlx_repair.py and the two boot scripts; timeout goes to 40 minutes for the in-flight boot, which waits out the torch warm before the self-heal is even scheduled. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * CI: let boot-studio-api-only.sh actually boot API-only The script exports UNSLOTH_API_ONLY=1 and its banner says --api-only, but nothing in unsloth_cli reads that variable. Whether the web UI is served is decided by the CLI flag alone; the backend only reads the variable back out in main.py to pick a CORS profile, after run.py has set it from the flag. Every caller has therefore been booting a server that serves the frontend, which is what the Playwright UI smokes need, so the flag is opt-in and no existing caller changes. The capability-verdict boots in mlx-ci.yml are the first caller without a built studio/frontend/dist (it is gitignored, and that job installs with pip rather than install.sh). Without the flag the server printed "Unsloth frontend build not found" and exited before binding, which is how this surfaced. Also in the smoke driver: give up a wait as soon as the server process is gone rather than spending the full budget, and flush the boot banner when it is written so it does not surface minutes later next to the timeout it preceded. * Studio: drop the verdict cache and bound both halves of the MLX repair hold Remove the localStorage hardware-verdict cache. It did not buy what it claimed: the store deliberately kept `fetched` false, so `capabilitiesUnknown()` is still true on a returning user's first paint and the rows spin exactly as they do without it. Its only demonstrated effect was unbreaking a `/settings` deep link, and it introduced a real regression in exchange. model-selector.tsx, pickers.tsx, model-inspector.tsx and app-sidebar.tsx read `chatOnly` with no `capabilitiesUnknown()` gate, so a cached `chatOnly=false` during a repair window offers MLX-only models that cannot load - worse than the conservative `navigator.platform` guess it replaced. env.ts and both frontend test files are back to what main has, and nothing under studio/frontend changes in this PR. Start the pre-start handoff grace on the first reading that sees the warm stopped rather than the last reading that saw it running. The warm's final stages are C-extension imports that hold the GIL for seconds at a time, so health requests queue behind them and the next one served can be the first in a minute. Measured from the last observed poll the grace could already be spent by the time anyone could ask, publishing the mlx_unavailable verdict during the very handoff the hold exists to cover. A stopped reading between two running ones is a lull, not the end, so it reopens. Bound a live repair worker with _WORKER_BUDGET_S, the subprocess timeout plus 300s. attempt_mlx_repair times the uv call but not the mlx_stack_available() imports that verify the install nor the detect_hardware() pass after it, and those import mlx.core, mlx_lm and mlx_vlm on a stack this module already assumes can park indefinitely. An alive thread was an unbounded answer, so a parked worker held the verdict provisional for the whole session. Fix _superseded_by_mlx_repair and mlx_repair_in_flight docstrings, which claimed a cap that did not exist. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
7e47cc03c1
|
Extract the API-only Studio boot into one script (#8029)
Twenty-one steps across eight workflows ran the same five-line body, varying only in port, log filename and the name of the environment variable the pid lands in. .github/scripts/boot-studio-api-only.sh takes those three as flags. Three parts of that body are easy to get subtly wrong, which is the argument for one definition rather than twenty-one: rm -rf, not `reset-password`. The boot only re-seeds a fresh .bootstrap_password when the auth directory is absent, so a caller that resets keeps the old password and the test authenticates against stale state. The pid has to reach $GITHUB_ENV, because the step that stops the server is a different step with a different shell. Both streams go to the log. The server writes startup diagnostics to stderr, so a redirect without 2>&1 leaves an empty log on exactly the failure someone needs it for. Not folded into serve-unsloth-run.sh, despite the plan saying so. That script boots `unsloth run --disable-tools` and its body is banner-key parsing and /v1/models resolution; these boot `unsloth studio --api-only` with auth wiped and no banner. The two share the word boot and nothing else, and each header now says which is which. Deliberately does not wait for health: the callers' waits differ, some polling /api/health and stopping while others go on to rotate the password and load a model in the same step. Left alone: three boots inside Playwright retry loops, which re-boot mid-loop and reassign the pid in the running shell, and three boot-briefly-and-kill steps that keep the pid in a local variable rather than $GITHUB_ENV. Each caller's paths filter gains the script, so an edit to it re-runs the workflows that call it -- the trap #8015 hit when the install body moved out from under those filters. |