Local UI to run and train LLMs and diffusion models. Supports GGUF, MLX, Qwen3.8, Kimi K3, MiniMax-H3, Gemma 4, FLUX and more.
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
Make the multi-turn smoke actually assert that history reached the model (#10009) The four-turn smoke says turns 2 and 4 are answerable only from the earlier turns, so a server that drops history fails rather than returning something plausible. Turn 4 did that. Turn 2 did not. gemma-3-270m-it, the model this smoke loads, answers 'What did I ask before?' with 'I am doing well! How can I help you today?' whether or not the history is attached, and nothing asserted that reply. The only content checks were '1' in turn 1 and 'paris' somewhere in the joined transcript, both satisfied by turns 1 and 3 alone, so a server that dropped every turn before the last still passed. Turn 2 now asks for the answer rather than the question, and the expected value is read from turn 1's own reply rather than a literal, so a turn 2 that contradicts turn 1 fails too. Turn 1 asks 58+27 rather than 1+1, because deriving from a fixed 1+1 reduces to hardcoding '2', which a history-less server can produce by luck. The turn-1 assertion becomes 'contains a number', which is what it was really for. Measured against llama-server b10687-mix-67dfc8b with the GGUF the workflow loads. Planting an explicit marker does not work on this model: 'What number did I ask you to remember?' answers 'I don't remember the number you asked me to remember.' An unguessable arithmetic answer does: turn 1 gives '58 + 27 = 95', turn 2 carries 95, and turn 2 sent with no history answers 'I am doing well, thank you for asking!', which carries no number and fails the check as it should. Also updates tests/studio/test_smoke_workflows_share_one_script.py, which workflow-trigger-lint runs unfiltered on every pull request, and adds coverage for a dropped turn 2 and for a turn 2 that contradicts turn 1. This does not claim to fix the intermittent determinism failure on the same turn. Ruled out while investigating: #9114, whose changes are confined to the Responses API and unreachable from Chat Completions, and llama.cpp, which reproduced identical transcripts on b10360 and b10687 with cache_prompt both ways across three thread and batch configurations. | 14 小时前 | |
Use Unsloth for bare Studio product references (#9732) * Studio: use Unsloth for product shorthand * Tests: restore case-folding fixture path * Complete Unsloth product reference rename * Tests: restore internal Studio references | 4 天前 | |
images: use narrower Discord button and drop duplicate (#5552) Two near-identical Discord button images existed under images/, with the only effective difference being the rendered button width. Keep the narrower variant (formerly the lowercase "discord button.png") and remove the wider "Discord button.png", consolidating to a single "Discord button.png" file. | 3 个月前 | |
Fail CI on a new exec/eval/compile of a value that is not written out (#9999) * Fail CI on a new exec/eval/compile of a value that is not written out The two holes fixed in unsloth-zoo#1108 and unsloth#9777 were the same shape: exec of an HTTPS response body, and exec of a string built from a downloaded config. CodeQL runs on this repository with Python enabled and raises py/path-injection here, but it has never raised py/code-injection, so the class is not covered by what is already running. The rule is the blunt one: the first argument to exec, eval or compile must be a written-out string. The 72 existing call sites are recorded in scripts/exec_literals_baseline.json, each with a reason, so the gate starts green and only new sites fail. An entry carries a digest of the call's own source rather than a line number, plus a count, so moving code does not churn the baseline and a new call cannot hide behind a removed one. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Name the encoding when reading the workflow file tests/test_source_read_encoding.py gates every read of a checked-in file on an explicit encoding, so the file does not break on Windows the day it gains a non-ASCII byte. --------- Co-authored-by: danielhanchen <elliegouldingstuff@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 18 小时前 | |
Give the cancel drain a budget sized for a loaded runner (#10008) Backend CI (Python 3.13) failed on main 8849f481d: test_non_streaming_gguf_cancel_drains_worker - assert False That assert is `await asyncio.to_thread(started.wait, 1.0)`, so what timed out is a THREAD START. It is a bound on the scheduler, not on the code under test, and the job was running the rest of the backend suite beside it. What this test asserts is that cancelling the request drains the worker. None of the numbers are part of that claim: they only decide how long to wait before calling it hung. So they move to one constant, 30s, sized for a loaded runner rather than for the ~5ms this takes when it works. The last assertion also stops being a sample. Cancelling the task unblocks the awaiting coroutine; it does not join the worker, which is off polling cancel_event every 5ms and only then sets `released`. Reading `released.is_set()` the instant the await returns is a race that happens to be won on an idle box. It is waited on now, because the drain is the point, not whether it had already finished by the time the test looked. It keeps its teeth. With the worker changed to ignore cancel_event entirely, the test fails, taking 50s to do it: FAILED ...::test_non_streaming_gguf_cancel_drains_worker 1 failed, 346 deselected in 50.05s Honest about what this is: a robustification, not a demonstrated fix. The original failure did not reproduce here. Pinned to one core against 16 spinners the test still passed, taking 173s against a normal 4.4s, a 39x slowdown that never came near the one-second bound. One failure in eight runs on main, green on the four commits before it. The budgets were incidental either way, and widening them costs a test that is already only slow when something is wrong. studio/backend/tests/test_openai_tool_passthrough.py: 347 passed. Co-authored-by: danielhanchen <unslothai@gmail.com> | 13 小时前 | |
Share the fixture backend version with the model-download E2E (#10002) * Share the fixture backend version with the model-download E2E #10001 fixed the portability smoke and left the sixth AppImage lane failing for exactly the reason it had just fixed. Dispatching the workflow against the published v0.1.804-beta showed five lanes green and one still red: AppImage ubuntu-22.04 success AppImage ubuntu-22.04-no-gles success AppImage ubuntu-24.04-wayland success AppImage debian-12 success AppImage fedora-44 success AppImage model download E2E failure with its own artifact carrying the same verdict: install probe result Stale { reason: "desktop_backend_version_outdated" } desktop_preflight completed disposition=ManagedStale port=None start_managed_repair command called appimage_model_download_webdriver.py had its own copy of the helper that reads MIN_DESKTOP_BACKEND_VERSION and reported it at two call sites, so a released build pinned to 2026.8.22 read the floor as outdated, reinstalled, and never sent the POST the test waits for. It recovers to Ready fifty seconds later, off the back of a full reinstall, which is well past the point the assertion has already failed. So the constant moves to appimage_test_support.py, which both files already import, along with the floor guard. Two copies of a fixture detail is what let one of them be fixed and the other not, and a third packaged-app test would have copied whichever it happened to be looking at. The guard raises rather than asserts now, so it survives -O, and it compares the leading component: a string compare would read "999" as above "9999", which is the one answer it must not get wrong. Setting the floor to 9999.1.1 fails it by name. Behaviour is unchanged for the portability smoke: still `install probe result Ready` against the real released AppImage. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Drop the import re that the removed helper was the only user of Source lint caught it: [BLOCKER] tests/studio/appimage_model_download_webdriver.py: HOISTED-IMPORT-UNUSED 're' (['import:re']) was used before, now unused _minimum_backend_version() was the file's only re.search, and it moved to appimage_test_support.py with the constant it fed. --------- Co-authored-by: danielhanchen <unslothai@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 13 小时前 | |
Parse the downloaded model mapper instead of exec'ing it (#9777) * Stop the fetched model mapper and an inherited dtype variable reaching exec unsloth-zoo#1083 closed one arbitrary-code-execution path: a model_type read out of a downloaded config.json was interpolated into a string that was then exec'd. This is the audit of the rest of this repo. Most sites are fine and are left alone: they execute source this package generated itself, from inspect.getsource of an installed transformers / TRL / PEFT class or from dir() of an installed module. Two are not. 1. loader_utils.py:_get_new_mapper runs on any from_pretrained("org/name") whose name is not in the shipped tables. It fetched a slice of mapper.py over HTTPS and exec'd the response body, so anything that could answer or intercept that request executed code in the user's process, for a download the user never asked for. The three targets in that slice are plain dict literals, so it now ast.parses the text and literal_evals the assignments instead. A body that does not carry them returns empty tables, which the caller already handles. The timeout, the offline check and the throwaway namespace are unchanged. The module-level derivation loop in mapper.py moved into build_mappers() to make that possible. The five generated tables were checked against main entry by entry: 448 / 1210 / 760 / 178 / 178, identical. 2. vision.py read UNSLOTH_FORCE_CUSTOM_DTYPE and eval'd two of its fields, then exec'd two more. Six model families depend on it, so it stays, but an inherited environment can no longer feed it: the dtype fields resolve through a fixed alias table, the six producers in loader.py register through register_custom_dtype, and only a value this process registered has its code fields executed. An inherited value is neutralised at import, because a zoo old enough to still eval that field can already be installed. The wire format is deliberately unchanged, since unsloth_zoo parses the same variable and the two packages release separately. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Read update() on the fetched tables, and tighten the import-light guard Three fixes, each reproduced first. The mapper probe read a subscript assignment but not the ordinary FLOAT_TO_FP8_ROW_MAPPER.update({...}), so a newer mapper.py written that way left the table empty and the models it had just added got no upgrade notice. Only a receiver naming one of the five exported tables and a literal mapping argument are read, so the fetched text still supplies nothing but data. The import-light guard recognised a loader helper only when it was imported under an alias, so load = importlib.import_module then load("torch") was missed and an undeclared heavy dependency could make the light runner fail at collection while the guard stayed green. Assignment aliases are now recorded too, including a chain. The same guard redirected a whole file for a body that calls pytest.importorskip("torch") and only then imports torch. That skip fires first on the light runner, so the import is never reached, and redirecting hid the unrelated light tests sitting next to it. Only an unconditional guard at the top of the body counts; one inside an if or a try does not. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Close four gaps found in review Mapper probe. A lambda is an expression, so it seeded the expression walk even though the walk refuses to descend into one it meets as a child. An `unused = lambda: MAPPER.update({...})` in a fetched mapper had its body read as installed, and the probe then told the user to upgrade for a mapping the newer file never installs. The same exclusion now applies at the seed. Custom dtype. UNSLOTH_FORCE_CUSTOM_DTYPE can be populated after this package is imported, by a dotenv or config loader, and the import-time neutralization has already run by then. trusted_custom_dtype() still refuses to run its code fields, but unsloth_compile_transformers runs before that check and reaches unsloth_zoo, whose supported older release still calls eval on the dtype field. The value is sanitized again at that entry point; the pass is idempotent. Import-light guard. It read only the statement spellings inside a test body, so a body-level importlib.import_module("torch") left the file on the runner that has no torch. It now applies the same dynamic-loader classification the module-scope scanner already used, with importorskip still exempt. And a guard was accepted wherever it appeared in the body, so `import torch` followed by pytest.importorskip("torch") counted as guarded when the light runner fails at the import first. The scan stops at the first statement that loads anything heavy. security-audit.yml now names unsloth/models/_utils.py, since a suite it runs reads that module. tests/security: 500 passed, 6 skipped. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Read the aliases a newer mapper adds inside build_mappers build_mappers is called at module scope, so its body runs at import, and the aliases that cannot be derived from the source table live in there. The installed builder cannot know one a newer mapper.py adds, so the probe was missing exactly the models main had just added and stopped offering the upgrade notice for them. Only the literal helper calls from that body, and only when the fetched module really does call the builder: a def that nothing calls still runs nothing. Not the rest of its body, which starts by binding its own empty tables and would have been read as the fetched module replacing the exported ones. tests/security: 502 passed, 6 skipped. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep loader origins, and apply builder aliases where the builder runs A renamed importlib helper was being read as a pytest skip guard: both live in the same alias table and only the pytest one suppresses the dependency, so from importlib import import_module as load left the file on the runner where that call fails. The table now records what each alias names. The builder's literal aliases are applied at the point the module calls build_mappers, not after every other module statement. A fetched mapper that calls it and then rebinds a table leaves that table empty, and reapplying the aliases afterwards reported support the newer file does not have. tests/security: 504 passed, 6 skipped. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Run the torch-dependent security suites where the rest of tests/security runs main moved `pytest tests/security` onto the workflow-trigger lint runner and added tests/studio/test_short_job_absorption.py to keep it from being run twice. This branch's job for the four suites that runner has to --ignore was in security-audit.yml, which is now exactly the second runner that rule forbids. The job moves into workflow-trigger-lint.yml beside the step that ignores those files. It keeps its own hardened runner and its CPU torch install, and it gains that workflow's unfiltered on.pull_request, which is a wider trigger than the paths filter it had. Its two guards follow. "Runs somewhere else" now asks per STEP rather than per file, since the redirect legitimately lives in the same workflow; "triggered by what it protects" accepts a host with no paths filter outright, which is what this one is, and still checks the filter when there is one. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Read the probe's builder call in execution order, and canonicalise dtype aliases Four review findings on the product code. The probe: `builder_called` is decided from the executed nodes rather than a raw walk, so a `build_mappers(...)` in dead or deferred code no longer reaches the fallback that applies the builder's aliases anyway; an unconditional `continue` ends a suite exactly as `break` does; and a helper call spelled with keywords - `_add_with_lower(mapper = ..., key = ..., value = ...)` - is read, while one that binds a parameter twice is skipped because it raises. The sanitizer: an accepted dtype field is written back in its canonical `torch.` spelling. `unsloth_zoo==2026.8.15`, the release this package's floor resolves to, still `eval`s that field, and `eval("fp16")` is a NameError that stops a load having nothing to do with the inherited value. An empty field stays empty, which is what an unset one already looks like to both readers. tests/security: 520 passed, 6 skipped. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Read the builder call and its source table where they actually run Three fixes, each with a test that fails without it. - The builder search walked the emitted parent statement, which descended back into the deferred children _executed_nodes had already excluded, so `unused = lambda: build_mappers(...)` applied the fetched builder's aliases. It now matches the call node itself, which is yielded whenever it really runs. - The source table is read where the builder call runs rather than as the last binding in the file, so a mapper that builds its exports from one table and then rebinds `__INT_TO_FLOAT_MAPPER` is no longer reported backwards. An earlier empty binding still loses to the real one, as before. - The import-light guard pruned lambdas by refusing to push them, which left the one seeded from the statement itself unpruned and dropped lambda defaults, which do run at definition time. Both follow from expanding a lambda to its defaults wherever it is met. * Read the source table as the builder receives it - `__INT_TO_FLOAT_MAPPER.update({...})` and a subscript assignment before the builder call now reach `build_mappers`. Only up to the call: a mutation written after it does not change what the builder was handed. - An empty or absent source table no longer ends the probe. A row-only FP8 entry cannot be expressed through the source table at all, so the base tables are built empty and the ordered mutation pass runs. A body that installs nothing is still reported as nothing found, decided from the result. * Replay deletions when reading the fetched source table `del __INT_TO_FLOAT_MAPPER["vendor/removed-bnb-4bit"]` before the builder call takes the entry away, and replaying only the additions left the probe reporting support for a model the newer mapper had just dropped. Deleting the whole name leaves an empty table rather than falling back to the last binding in the file. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Read the builder call that populates the exports, and replay its deletions - The probe now picks the executed `build_mappers(...)` whose result binds the five exported tables, rather than the first one that runs, so a validation call over a dummy table before it is no longer read as the source. - `return` terminates a suite in the executed-node walk, and a decided branch that ends in a jump ends the suite around it, so an addition written below an unconditional return inside the builder is not installed. - A literal `del` on an exported table replays with the assignments and the `update()` calls, so an alias the newer mapper removed is not reported. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Read the last export-producing builder call, and install Triton in the CPU job - Rebuilding the five exports twice leaves the SECOND result installed, so the probe now reads the last export-producing call rather than the first. - The executed-node walk reports WHAT ended a suite, so a `while True:` body that returns ends the function while one that only breaks leaves the statements after the loop standing. - The torch-dependent security job installs Triton: `unsloth/_gpu_init.py` imports it unconditionally and the CPU torch wheel does not supply it, so collection failed before any suite ran. `download-r2.pytorch.org` joins the egress allowlist, which is where the CPU index redirects and where the job was actually failing. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Anchor the mapper replay to the selected builder call, and read assigned guards - The source-table replay and the builder's own aliases are both applied at the call that populates the exports, matched by identity, rather than at the first builder call that runs. A validation call above a mutation no longer makes the probe build from stale data, and a rebind written between two calls no longer wipes aliases the fetched builder installs. - Loader aliases are bound in source order with the last binding winning, so an imported `importorskip as load` stops being a skip guard once `load` is assigned the real importer, and a binding to something that is not a loader takes the name out of the table. - `torch = pytest.importorskip("torch")` counts as an unconditional guard, so a later import in that body no longer moves the whole file - and the unrelated import-light tests in it - onto the heavy runner. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Scope loader aliases lexically, and reset the exports at the rebuild Four fixes from the latest review round. The import-light guard stopped at a non-call statement that loaded a heavy dependency, but let a call that was not a skip guard past unchecked, so a dynamic import above a later importorskip kept the file on the light runner. Its alias table was also built from one walk over the whole file, so a binding inside one function decided how a call in an unrelated one was read; it is now one table per lexical scope, each built on its enclosing one. The mapper probe replayed mutations written above the selected builder call onto its result, although that assignment replaces all five exported tables, so an entry added between two rebuilds was reported as installed. It also marked a class-bound name shadowed from the class body first line, dropping a mutation written above the binding that really does reach the module global. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Read an empty export literal as an initialiser when there is no builder Against main's mapper.py today the probe returned empty tables, so the "please update Unsloth" notice never fired for any model. That file has no build_mappers: it writes the five exports as empty literals and fills them from a module-scope loop, and the whole-name assignment rule read those literals as the module clearing the tables. An empty dict assigned to an exported table is now an initialiser rather than a clear, but only where the fetched module has no build_mappers call, since that is the shape whose exports are built by the loop this reproduces with the installed builder. A non-empty rebind still replaces. Checked against the live file: the probe and an exec of the same text now agree on all five tables, 448/1210/760/178/178, for both mapper.py shapes. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Cut the comments back to what a reader cannot infer Comment-only pass: 1106 added comment lines down to 403, no code tokens changed (comment_tools.py check --strip-docstrings, 11/11 OK). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 22 小时前 | |
Give the in-venv CLI tests a writable STUDIO_HOME (#9993) * Give the in-venv CLI tests a writable STUDIO_HOME Eleven tests in unsloth_cli/tests have been failing on main, taking Repo tests (CPU) down with them on every pull request: Error: could not coordinate the Unsloth launch: [Errno 13] Permission denied: '/fake' These tests pin STUDIO_HOME to /fake/studio so sys.prefix.startswith() picks the in-venv branch. /fake was fine while nothing on the path touched the disk. #9890 put _studio_runtime_launch_guard in front of the launch, and studio_runtime_launch_guard does studio_home.mkdir(parents = True) and then locks a file inside it, so an unwritable home now aborts the run before run_server is ever reached. Every one of these failures is the same shape underneath: an empty captured dict, because the flag under test never got as far as being forwarded. The guard is right to create the directory it locks in, and right to refuse the launch when it cannot. What was wrong was a fixture using an unwritable sentinel path for somewhere that is now genuinely written to, so the tests move to tmp_path. The tree keeps its shape, so sys.prefix still sits under STUDIO_HOME and the in-venv branch is still the one exercised: removing the sys.prefix pin still fails these tests. unsloth_cli/tests: 1113 passed, 6 skipped. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Anchor the node-dir extraction on NODE_DIR, and cover the staging branch Second failure in the same job, from the same pull request, and it only became visible once the pytest step above stopped failing first. tests/sh/test_studio_home_node_dir.sh lifts the real STUDIO_HOME and NODE_DIR logic out of studio/setup.sh by content anchor and runs it against a fake HOME. Block B started grabbing at: ^if \[ "\$_STUDIO_HOME_IS_CUSTOM" = true \]; then #9890 added a staging branch above that one: if [ -n "$STAGE_ROOT" ]; then _NODE_PARENT="$RUNTIME_ROOT" elif [ "$_STUDIO_HOME_IS_CUSTOM" = true ]; then which demoted the anchored line to an elif. The anchor then matched nothing, block B came out empty, and the file's own extraction guard fired: FAIL: blockB extraction broke setup.sh is right here. A background update has to put its Node under the staging root, not into the home the running Studio is serving from. The anchor was the problem: it pinned one branch's text in a conditional that was always going to grow branches. Block B is now anchored on the NODE_DIR assignment, the line that actually matters, and walks back to the top-level `if` feeding it. Branches can be added, reordered or removed without touching this file. Walking back could in principle land on an unrelated `if`, so the extraction guard now also requires both _STUDIO_HOME_IS_CUSTOM and STAGE_ROOT to appear in what it captured. The staging branch was untested, which is why nothing caught this at the time, so it gets two checks: a stage root sends Node under it, and it outranks a custom studio home. Both drive UNSLOTH_STUDIO_STAGE_ROOT, the public knob, since block A derives STAGE_ROOT and RUNTIME_ROOT from it. 7 checks, all passing. Deleting the staging branch from setup.sh fails the file. * Pin UNSLOTH_ALLOW_CPU for the child that imports unsloth Third failure in Repo tests (CPU), and it now runs before the other two, so it is what the job reports on main today while the eleven unsloth_cli failures and the setup.sh anchor wait behind it. test_a_hostile_dtype_field_cannot_reach_an_eval, added by #9777, spawns a fresh interpreter that imports unsloth. On a host with no torch accelerator that dies inside unsloth_zoo: File "unsloth_zoo/device_type.py", line 389, in get_device_type raise NotImplementedError("Unsloth cannot find any torch accelerator? You need a GPU.") so the child never reaches the module under test and the returncode assertion fires. The FutureWarning about HF_HUB_ENABLE_HF_TRANSFER that heads the failure message is unrelated noise from the same stderr. UNSLOTH_ALLOW_CPU=1 is the documented way to import on such a host, and the test was getting it by accident: studio/backend/tests/conftest.py sets it with setdefault at import, so whether the child inherited it depended on whether that conftest had been loaded, which depends on what else the run collected. In this job it had not been. The test copies os.environ and now pins the variable in the copy. It is a test about the dtype field, and it should read the same on a runner with a card and on one without. With no accelerator visible and the variable unset in the parent: 1 failed, 11 passed before, 12 passed after. Unchanged with a card present. --------- Co-authored-by: danielhanchen <unslothai@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 19 小时前 | |
Use Unsloth for bare Studio product references (#9732) * Studio: use Unsloth for product shorthand * Tests: restore case-folding fixture path * Complete Unsloth product reference rename * Tests: restore internal Studio references | 4 天前 | |
Use Unsloth for bare Studio product references (#9732) * Studio: use Unsloth for product shorthand * Tests: restore case-folding fixture path * Complete Unsloth product reference rename * Tests: restore internal Studio references | 4 天前 | |
pre-commit CI config (#3565) | 9 个月前 | |
fix(studio): verify TLS against the OS trust store at runtime (corporate TLS-inspection proxies) (#8108) * fix(studio): verify TLS against the OS trust store at runtime On macOS/Windows behind a TLS-inspecting proxy (Cisco Umbrella, Zscaler, ...), every huggingface.co request from the Studio backend fails with CERTIFICATE_VERIFY_FAILED: Python verifies against certifi's bundled Mozilla roots and ignores the OS trust store where the proxy CA lives. A terminal user can export SSL_CERT_FILE, but macOS .app / desktop-shortcut launches never read shell profiles, so GUI-launched Studio has no workaround. Inject truststore into ssl at each network-touching entry point (server, HF download worker, STT download worker, training worker) — injection is per-process and does not survive spawns. Default-on for macOS/Windows, opt-in on Linux, opt-out anywhere via UNSLOTH_STUDIO_NATIVE_TLS=0, mirroring UV_NATIVE_TLS in install.sh (#6671), whose runtime counterpart this is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(studio): cover inference/export workers and the studio extra Review follow-ups: the inference worker resolves remote LoRA bases and the export worker pushes/loads from the Hub, both in fresh spawned interpreters that missed the injection; and pyproject's studio extra must mirror requirements/studio.txt (test_studio_extra_matches_requirements), so truststore is added there too for `pip install "unsloth[studio]"`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(studio): propagate native TLS to remaining children, gate 3.9 Second review round: the data-recipe jobs worker and the transformers -c config probe are further fresh interpreters with Hub/provider traffic, so activate in the worker and inline the gated injection in the probe script (it cannot import backend modules). uv/pip child installers do their own TLS (rustls ignores in-process injection), so activation now exports UV_SYSTEM_CERTS/UV_NATIVE_TLS, mirroring install.sh. truststore requires Python >=3.10 while the project floor is 3.9, so the dependency gets a python_version marker on both sides of the requirements mirror; activate_native_tls() already falls back to certifi where the import is unavailable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(studio): activate native TLS in diffusion trainer * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix uv TLS opt-out and remaining uncovered spawns for PR #8108 - native_tls: mirror one resolved value across UV_SYSTEM_CERTS/UV_NATIVE_TLS. uv takes either variable as an opt-in, so UV_SYSTEM_CERTS=0 alone was re-enabled by the legacy name defaulting to 1. Matches install.sh. - native_tls: warn instead of debug when injection fails, and document that explicit CA bundles become additive rather than exclusive under truststore. - model_config: activate in the _VISION_CHECK_SCRIPT child, which calls AutoConfig.from_pretrained and fails quiet, so VLM detection silently degraded behind a proxy. - prebuilt_core: activate at import so the llama.cpp / whisper.cpp installers and the resolve-prebuilt probe verify their urllib downloads the same way. - mlx_repair: forward the UV cert variables through the allowlisted child env, the one uv installer that did not inherit them. - diffusion_training_service: activate inside run_without_native_path_secret so truststore imports after the lease secret is scrubbed, and scrub the parent env before the child starts, like the other orchestrators. - tests: pin the uv mirror both ways, stop leaking the UV vars into the pytest process, assert the diffusion ordering, and add static coverage that every spawned entry point still activates. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep the vision probe's helper block intact for PR #8108 test_vision_subprocess_unions_sidecar_registry slices the embedded script at its first 'try:' to exec the inlined helpers, so putting the TLS activation above them left that slice empty. Move it below the helpers, still ahead of AutoConfig.from_pretrained. * Cut the native TLS log noise and pin the client-only limitation for PR #8108 Simulation findings on the branch: - A failed injection logged a full traceback at warning level. On Python 3.9, where truststore is excluded by the dependency marker on purpose, that is one stack per worker start. Now one line carrying the reason. - truststore's injection is client-side: an SSLContext built after it verifies a peer chain on every handshake, so it cannot serve TLS. Studio serves plain HTTP on loopback so nothing breaks today, but an in-process HTTPS listener added later would fail at handshake on the default-on platforms. Documented, and test_native_tls_entrypoints.py now fails if one appears. * Tighten native TLS comments * Cap truststore at the next minor truststore is pre-1.0 and 0.x minors are where it has changed verification behaviour (0.9 to 0.10), while fixes land as patches inside a minor (0.10.1, 0.10.3, 0.10.4). Since activation applies process-wide to every TLS connection, a semantic change should get a human look rather than arrive on the next install. 0.10.4 has been the current release for a year, so nothing is held back today. Mirrored in both declarations. * Generate the inline native TLS gate from native_tls.py The python -c probes and prebuilt_core.py cannot import backend modules, so each carried its own hand-written copy of the UNSLOTH_STUDIO_NATIVE_TLS gate. Three copies of the same platform defaults drift silently: a child would keep downloading against certifi while everything else used the OS store. native_tls.inline_gate_source() now renders the gate from the same constants native_tls_enabled() reads. The probes concatenate it, and prebuilt_core.py keeps a paste that test_native_tls_entrypoints.py asserts verbatim against the generated text. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Compare the prebuilt TLS gate by AST, not by text ruff-format restyles the pasted gate in prebuilt_core.py (quote style, line wrapping), so a substring assertion against inline_gate_source() fails the moment the formatter runs, as it did on 668cd9f76. Compare the parsed statements instead: still catches a real edit to the platform defaults, and ignores formatting that cannot change behaviour. * Vendor truststore instead of depending on it truststore was a new third-party dependency for every Studio user, added to fix CERTIFICATE_VERIFY_FAILED for the few behind a TLS-inspecting proxy. Nothing in Unsloth required it before, directly or transitively. It is MIT, six pure-Python files, no dependencies of its own, so vendor it at studio/backend/vendor/ and drop both declarations. pip vendors it for the same reason. utils/third_party_source.py is not an option here: it downloads over urllib at first use, which behind the very proxy this fixes would itself fail to verify. The copy has to exist before the network does. Consumers append the vendor directory to sys.path and import the top-level name, so a truststore the user installed themselves still wins. The directory is deliberately not a package: a dotted import would load the same files under a second module name, and each would wrap ssl again. test_vendored_truststore.py pins the tree against the published 0.10.4 wheel and scripts/sync_vendored_truststore.py re-syncs it, since vendored code is invisible to the dependency scanners. * Tighten native TLS comments * Drop the truststore sync script and treat the copy as static Nothing in Studio ever ran it: it only re-downloaded a wheel for a maintainer bumping the pin, which implies a refresh story this repo does not have. The vendored files are a static copy. truststore_manifest.json and test_vendored_truststore.py stay, since they are what makes them static: they pin the tree to the published 0.10.4 hashes and fail if anything edits it. A version bump now means replacing the tree and updating the manifest in the same commit, deliberately, which the README spells out. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <moonshotaisubstack@gmail.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> | 21 天前 | |
Update CODE_OF_CONDUCT.md | 9 个月前 | |
Revert "Write down the rule for UI changes to Studio, and put it on every pul…" (#9544) This reverts commit 963639df6a24d7510152b77ebca8ef458044d522. | 7 天前 | |
Rename cli/ to unsloth_cli/ to fix namespace collision with stringzilla (#4393) * Rename cli/ to unsloth_cli/ to fix namespace collision with stringzilla stringzilla installs a namespace package at cli/ (cli/split.py, cli/wc.py) in site-packages without an __init__.py. When unsloth is installed as an editable package (pip install -e .), the entry point script does `from cli import app` which finds stringzilla's namespace cli/ first and fails with `ImportError: cannot import name 'app' from 'cli'`. Non-editable installs happened to work because unsloth's cli/__init__.py overwrites the namespace directory, but this is fragile and breaks if stringzilla is installed after unsloth. Renaming to unsloth_cli/ avoids the collision entirely and fixes both editable and non-editable install paths. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update stale cli/ references in comments and license files --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 5 个月前 | |
Rename cli/ to unsloth_cli/ to fix namespace collision with stringzilla (#4393) * Rename cli/ to unsloth_cli/ to fix namespace collision with stringzilla stringzilla installs a namespace package at cli/ (cli/split.py, cli/wc.py) in site-packages without an __init__.py. When unsloth is installed as an editable package (pip install -e .), the entry point script does `from cli import app` which finds stringzilla's namespace cli/ first and fails with `ImportError: cannot import name 'app' from 'cli'`. Non-editable installs happened to work because unsloth's cli/__init__.py overwrites the namespace directory, but this is fragile and breaks if stringzilla is installed after unsloth. Renaming to unsloth_cli/ avoids the collision entirely and fixes both editable and non-editable install paths. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update stale cli/ references in comments and license files --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 5 个月前 | |
Update README.md | 3 天前 | |
Source the update popup's release notes from the GitHub releases (#8352) * Source the update popup's release notes from the GitHub releases CHANGELOG.md had to be hand-edited before every release, and it was keyed by the PyPI backend version while the announcement itself lives on a release tagged with the Studio version. The two drifted: the file carried three-bullet stubs while the release page carried the write-up. The newest published release is the source now, so the popup follows each new release with no file to edit and no rebuild. Only the announcement is shown: the install instructions, the generated What's Changed list, New Contributors, the Full Changelog line and the appended build provenance are stripped out, wherever in the body they were written. * Strip an install block introduced by a paragraph, not just by a heading v0.1.471-beta writes the same sentence v0.1.43-beta puts in a `###` heading with no hashes in front of it, so _is_upgrade never saw it and the popup kept a stale version pin and both install commands. A paragraph has no level, so the block it opens runs to the next heading that is not one of the platform headings holding its commands. Run over every published release body, exactly one output changes and the removed lines are exactly that install block. Also raise the releases page to the endpoint maximum of 100, which costs the same single request, and fix three assertions that could not fail: the draft case used a tag the tag filter rejects first, so the draft filter had no coverage; no published body carries a provenance section, so asserting its absence proved nothing; and "refresh" appears throughout the hook, so the retry path could be deleted with the test still green. The comment justifying the removed desktop fallback was wrong about what latest.json's `notes` holds. It is the static download blurb the release workflow writes, the same text every release, not this release's body. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bound the rate-limit deadline, not just the first wait on it The 15 minute cap was applied to the returned TTL while the raw reset epoch went into _rate_limited_until, so the fetch after that TTL expired answered from the uncapped deadline and blocked release notes, Retry included, for the whole server-supplied interval. GitHub says not to request again before X-RateLimit-Reset, so the reset still wins over the back-off rather than being cut to 15 minutes, which would retry into the limit. It is now held to the hour that the unauthenticated window actually is, so a skewed or proxied header cannot park the popup. * Record a deadline for every refusal, not just the primary rate limit A secondary rate limit answers 403 or 429 without X-RateLimit-Remaining: 0, so that branch returned a 15 minute TTL and left _rate_limited_until unset. Retry saw no lockout, dropped the cached failure and requested straight back into the limit, which is what GitHub warns against. Every refusal now records a bounded deadline, in the order GitHub documents: Retry-After, which is how a secondary limit states its wait, then the primary limit's reset, then the plain back-off when the response says neither. * Size the releases page against the read cap, and split platforms on a slash A release entry carries its whole body and the newest ones run about 40 KiB, so the endpoint's maximum of 100 puts the response near 4 MiB against a 2 MiB cap. The fetch would then fail outright with the release it wanted sitting at the top of the page it just threw away. Back to 30, which is about 1.2 MiB at that rate, with the arithmetic recorded in a test that fails if the page grows past what the cap allows. _is_platform replaced a slash with a comma but not the spaces around it, so neither "macOS / Linux / WSL" nor "macOS/Linux/WSL" matched and an install block written that way would have kept its commands. The separators are now read as one thing. * Tighten the comments this change added Comments only, no code. Mostly the test docstrings, which had grown into several lines of bug narrative each and now lead with the rule and keep at most a clause of what broke, and the blocks in release_notes.py that only restated the line below them. The reasons bought during review are kept, shorter: why the releases page is 30 and not the endpoint maximum, why the rate-limit deadline is bounded and why GitHub's reset beats the back-off, why releases are ordered by published_at, and why the install block is excised where it stands. * Refuse a desktop tag that goes backwards for PR #8352 Both updater paths compare SemVer, so v0.1.60-beta published after v0.1.527-beta reads as older and no client on the 5xx series is ever offered it. Desktop builds have only shipped at four tags, v0.1.526-beta through v0.1.61-beta, so this is the first time the tag numbering has stranded anyone, and it strands everyone installed before Aug 10. Check the new tag against the version in the published latest.json, which is the file a running build actually reads, and refuse a release that would strand its own users. Warns rather than fails when the manifest is unreachable, so a network blip cannot block a release. * Compare full SemVer in the tag guard, and drop a bare platform install block The guard compared only the numeric triple, so v0.1.528 or v0.1.528-rc after v0.1.528-beta read as equal and were refused, though both are newer and the updater treats them that way. Compare full SemVer precedence instead. Separately, a platform heading only ended an install block that an 'Updating' heading or paragraph had already opened, so a release heading its commands with a bare 'MacOS, Linux, WSL:' kept them. v0.1.0-beta and v0.1.41-beta do that, and they are the only two of the 24 published bodies this changes; all seven platform headings in that corpus head an install block. * Resolve the published desktop version from the newest Studio release The guard read /releases/latest/download/latest.json, but this repo also publishes llama.cpp prebuilts as ordinary releases: 25 of them, non-draft and non-prerelease, so any new one becomes GitHub's latest release. Those carry no manifest, so the read would 404 and the guard would skip itself silently, which is the same hazard release_notes.py already documents and avoids. Take the newest non-draft Studio tag that actually ships a latest.json, and authenticate with the workflow token rather than spending a shared quota. * Tighten the comments the last two rounds added --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 19 天前 | |
studio: add higgs, moss, and minimax audio models * Studio: add Higgs, MOSS, and MiniMax audio models * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten comments and source headers * Restore audio PR scope * Fix native audio review findings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix native audio platform edge cases * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix native audio review follow-ups * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Complete native audio cancellation guards * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix native audio inventory and cancellation follow-ups * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Secure MOSS codec selection and instructions * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Reduce verbose comments * Fix native audio review findings * Fix Audio model loading and managed downloads * Keep cached Audio loads available offline * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix native audio placement and inventory * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix MOSS generation and audio history * Forward MOSS language through audio worker * Fix cached native audio handoff * Use MOSS model context for audio duration * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix cached native audio handoffs * Validate native audio prompt budgets * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix native audio placement and routing * Handle resident VRAM during audio model handoff * Apply repository Python formatting * Trim native audio test coverage * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix backend CI after main merge * Fix teardown padding CI stub * Fix native audio review findings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fail closed on unknown llama gpu order * stage the higgs tts2 audio tokenizer * move the higgs tts2 codec to the model device * require remote code consent before audio staging * hide unsupported native audio adapters * load minimax components from the selected checkpoint * use safe attention backends for moss audio * fail closed on unavailable audio tokenizers * serialize audio generation and model swaps * use the released diffusers minimax support * neutralize moss tts prompt delimiters * honor speech request contracts * allow the full minimax music frame budget * expose native audio prompt controls * stabilize native audio generation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * preserve frame-aligned minimax durations * resolve higgs codecs from checkpoint metadata * neutralize minimax music delimiters * load approved higgs3 codecs with auth * fail closed on oversized audio metadata * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * align native audio security preflight * invalidate stale audio mode work * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * reject unsupported audio plans before download --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@users.noreply.github.com> Co-authored-by: mahiatlinux <110882203+mahiatlinux@users.noreply.github.com> | 2 天前 | |
Fix three CI regressions: code-span anchor, load-dispatch anchor, recordless pip install (#9959) * Fix three CI regressions: code-span anchor, load-dispatch anchor, recordless pip install Three unrelated failures, all reproduced locally first. 1. tests/studio/test_update_release_notes.py::test_code_span_closers_ignore_backslashes #9796 moved the code-span scanner into scanCodeSpans, which is defined ABOVE `export function codeSpans`. The test sliced the file from that export and counted `escaped(text` in the remainder, so the count went from 1 to 0 and the assertion "only an opener can be escaped" fired on a file where exactly one opener is still the only escaped thing. Count over the whole module and check the surviving call guards the opening run, so a wrapper moving again cannot read as "no opener is escaped either". 2. studio/backend/tests/test_parallel_slots_per_load.py::test_route_resolves_slots_once_before_dedupe_guard_and_load The GGUF load moved into _run_gguf_load_attempt, a health-wait helper defined above _load_model_impl, so `llama_backend.load_model` left the function body and .index() raised ValueError. Every ordering the test guards still holds. Anchor on whichever call the body dispatches a load through instead of on one backend's method name. 3. startup windows-latest pip will not replace a distribution whose .dist-info carries no RECORD, and an interrupted install leaves exactly that. The job reuses its venv, so the stub is not transient: once written, every later run dies the same way on "Cannot uninstall pydantic-core / no RECORD file was found". pip_install now retries once after clearing only the distributions pip named as unremovable, and only when it named one. pip's own hint is --ignore-installed, which would apply to every package in the command and skip real upgrades with it. run() grows check=False so pip_install can inspect a failure instead of exiting inside it; every existing caller keeps the old exit-on-failure behaviour. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Mirror the StageRoot branch into install.ps1's shared copy #9890 added staging support to studio/setup.ps1 and put an early `if ($StageRoot)` return inside Get-ManagedLlamaCppDir, which sits in the block install.ps1 holds a byte-identical copy of. The copy never got it, so test_denied_llama_cpp_preflight.py failed on exactly the drift it exists to catch, taking Repo tests (CPU) down with it. install.ps1 never assigns $StageRoot, and it runs under Set-StrictMode -Off from line 25 onwards, so the read is $null there and the branch is inert. Parity is what the block is for: the copies have to match or the next person edits one of them and only finds out in production. --------- Co-authored-by: danielhanchen <unslothai@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 23 小时前 | |
Bump install.sh / install.ps1 pins to unsloth>=2026.8.22 and unsloth-zoo>=2026.8.16 (#9868) Co-authored-by: Daniel Han <unslothai@gmail.com> | 3 天前 | |
Versioning | 3 天前 | |
docs: fix --quantization flag in unsloth-cli.py usage example (#7688) | 30 天前 |
Unsloth 是首个可以运行和训练模型的桌面应用。
⚡ 快速开始
下载适用于你操作系统的原生 Unsloth Desktop 应用:
| 平台 | 链接 |
| Windows | 下载 |
| macOS | 下载 |
| Linux / Ubuntu (deb) | 下载 |
| Linux (AppImage) | 下载 |
可从 Unsloth 或 GitHub Releases 下载。
或者,如果你更倾向于手动安装:
macOS, Linux, WSL:
curl -fsSL https://unsloth.ai/install.sh | sh
Windows:
irm https://unsloth.ai/install.ps1 | iex
社区:
⭐ 功能
Unsloth 支持 Windows、Linux、WSL 和 macOS。同时支持 多 GPU 配置、NVIDIA、AMD、Intel 显卡、CPU 以及 Vulkan 后端。
使用 AI 运行与构建
- 运行并训练大语言模型、扩散模型、嵌入模型和音频模型:Qwen3.8, Kimi K3, MiniMax-H3, Muse Glimmer, DeepSeek-V4, Gemma 4。
- 智能体与工具: 使用本地模型配合 Claude Code、Codex 和 MCP,支持工具调用和代码执行。
- 搜索与 RAG: 使用私密且无限制的网页搜索、深度研究、自动压缩(滚动上下文窗口)和 RAG。
- 图像与视频: 运行并训练 图像 和视频扩散模型或多模态模型
- 远程与 LAN: 从 LAN 中的任意设备访问本地模型,或通过安全的 Cloudflare HTTPS 远程访问。
- 连接: 通过 OpenAI 兼容 API 提供模型服务。也可接入你的 ChatGPT/Codex 订阅和 云服务提供商
训练与部署
- 微调: 训练大语言模型、扩散模型、TTS 和嵌入模型时,速度提升 2 倍,显存占用降低 70%,并且 无精度损失
- 完整支持: 支持 强化学习、LoRA、QLoRA、全量微调、预训练、RL、GRPO、DPO 和 FP8。
- 导出与部署: 以 GGUF、NVFP4、FP8 等多种格式 导出 或部署模型。
- 数据集: 使用 Data Recipes 从 PDF、CSV、DOCX 文件等构建数据集。
🚀 Unsloth Start
Unsloth Start 只需一条命令,即可将 Claude Code、Codex 及其他智能体接入本地模型。
unsloth start claude --model unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL
| 智能体 | 命令 |
|---|---|
| Claude Code | unsloth start claude |
| OpenAI Codex | unsloth start codex |
| Hermes Agent | unsloth start hermes |
| OpenClaw | unsloth start openclaw |
| OpenCode | unsloth start opencode |
📥 安装
Unsloth 可通过三种方式使用:Unsloth Desktop(桌面应用)、Unsloth Studio(Web UI)或 Unsloth Core(基于代码的版本)。
Unsloth Desktop(推荐)
| 平台 | 链接 |
| Windows | 下载 |
| macOS | 下载 |
| Linux / Ubuntu (deb) | 下载 |
| Linux (AppImage) | 下载 |
Unsloth Studio(Web UI)
macOS、Linux、WSL:
curl -fsSL https://unsloth.ai/install.sh | sh
Windows:
irm https://unsloth.ai/install.ps1 | iex
启动
unsloth studio
HTTP 安全部署
unsloth studio --secure
Docker
使用我们的 Docker 镜像 unsloth/unsloth 容器。运行:
docker run -d -e JUPYTER_PASSWORD="mypassword" \
-p 8888:8888 -p 8000:8000 -p 2222:22 \
-v $(pwd)/work:/workspace/work \
--gpus all \
unsloth/unsloth
远程 HTTPS 与局域网访问
服务端工具默认开启——请务必谨慎!妥善保管好密码,或在对外暴露 Unsloth 时使用 --disable-tools。
公网 HTTPS 访问: 会创建一个免费的 Cloudflare 链接来提供 Unsloth 服务——你可以在全球范围内访问该链接(甚至可以直接在手机上访问!)
unsloth studio --secure
-H 0.0.0.0 以及不同的端口也可以正常工作:
unsloth studio -H 0.0.0.0 -p 8888
局域网访问(家庭网络):Settings > API keys > LAN access
密码管理与无头启动
无头启动:
UNSLOTH_STUDIO_PASSWORD='your-strong-password' unsloth studio --secure # via env var
重置您的密码:
unsloth studio reset-password
开发、Nightly、卸载
如需查看开发、Nightly 及卸载等相关说明,请参阅高级安装。
Unsloth Core(代码方式)
Linux、WSL:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv unsloth_env --python 3.13
source unsloth_env/bin/activate
uv pip install unsloth --torch-backend=auto
Windows:
winget install -e --id Python.Python.3.13
winget install --id=astral-sh.uv -e
uv venv unsloth_env --python 3.13
.\unsloth_env\Scripts\activate
uv pip install unsloth --torch-backend=auto
AMD、Intel、DGX Spark、Blackwell:
请查阅我们的 Blackwell 指南 和 DGX Spark 指南。
若要在 AMD 和 Intel GPU 上安装 Unsloth,请按照我们的 AMD 指南 和 Intel 指南 操作。
📒 免费 Notebook
使用我们的 Notebook 即可免费训练。 阅读我们的 指南,添加数据集,运行后部署训练好的模型。
| 模型 | 免费 Notebook | 性能 | 内存占用 |
|---|---|---|---|
| Unsloth Studio | ▶️ 免费开始 | ||
| Gemma 4 (E2B) | ▶️ 免费开始 | 快 1.5 倍 | 减少 50% |
| Qwen3.5 (4B) | ▶️ 免费开始 | 快 1.5 倍 | 减少 60% |
| gpt-oss (20B) | ▶️ 免费开始 | 快 2 倍 | 减少 70% |
| Qwen3.5 GSPO | ▶️ 免费开始 | 快 2 倍 | 减少 70% |
| gpt-oss (20B): GRPO | ▶️ 免费开始 | 快 2 倍 | 减少 80% |
| Qwen3: Advanced GRPO | ▶️ 免费开始 | 快 2 倍 | 减少 70% |
| embeddinggemma (300M) | ▶️ 免费开始 | 快 2 倍 | 减少 20% |
| Llama 3.1 (8B) Alpaca | ▶️ 免费开始 | 快 2 倍 | 减少 70% |
| Llama 3.2 Conversational | ▶️ 免费开始 | 快 2 倍 | 减少 70% |
| Orpheus-TTS (3B) | ▶️ 免费开始 | 快 1.5 倍 | 减少 50% |
🦥 Unsloth 资讯
- AMD 训练:在 Windows、WSL 和 Linux 的 AMD GPU 上训练、运行 RL、对话和部署。 指南
- 任意智能体可用的本地模型:使用
unsloth start,通过 Unsloth 兼容 OpenAI 与 Anthropic 的 API,连接 Claude Code、Codex、Hermes、OpenCode、OpenClaw 等。 指南 - GLM-5.2:通过 Unsloth 动态 GGUF,在本地运行 Z.ai 的 744B 参数、1M 上下文开源模型。 指南
- DeepSeek-V4:在本地运行 DeepSeek-V4-Flash,修正多轮对话与工具调用行为。 指南
- Gemma 4:支持 QAT、MTP、GGUF 与 MLX,可运行并训练 Gemma 4 文本、图像与音频模型。 指南
- MCP 服务器:通过 Model Context Protocol 将本地模型连接至文件、应用、数据库和外部工具。 指南
- 新模型:Qwen-AgentWorld, Ornith, Kimi K2.7 Code 与 MiniMax M3
更多资讯
- 连接:在同一界面中混合使用本地模型与 API 提供商(OpenAI、Anthropic)或服务器(vLLM、Ollama)。 指南
- 发布 Unsloth Studio:我们全新的 Web UI,用于运行与训练 LLM。 博客
- DiffusionGemma:在 Unsloth Studio 中运行并微调 Google 的扩散语言模型,推理速度提升 1.8 倍。 指南
- Qwen3.6:使用 MTP 运行并训练 Qwen3.6,推理速度提升 1.4-2.2 倍,并为支持的 GPU 提供 NVFP4 量化。 指南
- 减少 35% VRAM,将 MoE LLM 训练提速 12 倍——支持 DeepSeek、GLM、Qwen 与 gpt-oss。 博客
- Embedding 模型:Unsloth 现已支持约 1.8-3.3 倍更快的 Embedding 微调。 博客 • 笔记本
- 相比其他所有配置,借助全新的批处理算法,实现 上下文长度提升 7 倍的 RL。 博客
- 全新 RoPE & MLP Triton Kernels 与 Padding Free + Packing:训练速度提升 3 倍,VRAM 减少 30%。 博客
- 500K 上下文:如今,在 80GB GPU 上即可训练具有 >500K 上下文的 20B 模型。 博客
- FP8 与视觉 RL:现在可以在消费级 GPU 上执行 FP8 与 VLM GRPO。 FP8 博客 • 视觉 RL
📥 高级安装
以下高级安装说明适用于 Unsloth Studio。如需 Unsloth Core 的高级安装,请查看我们的文档。
开发者 / 夜间 / 实验版安装:macOS、Linux、WSL:
开发者版安装会从 main 分支构建,该分支为最新(夜间版)源代码。
git clone https://github.com/unslothai/unsloth
cd unsloth
./install.sh --local
unsloth studio -p 8888
要将其安装到独立位置,请设置 UNSLOTH_STUDIO_HOME:
UNSLOTH_STUDIO_HOME="$PWD/.studio" ./install.sh --local
UNSLOTH_STUDIO_HOME="$PWD/.studio" unsloth studio -p 8888
然后更新:
cd unsloth && git pull
./install.sh --local
unsloth studio -p 8888
开发者 / 每日构建 / 实验性安装:Windows PowerShell:
开发者安装基于 main 分支构建,即最新的(每日构建)源代码。
git clone https://github.com/unslothai/unsloth.git
cd unsloth
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\install.ps1 --local
unsloth studio -p 8888
如需安装到独立目录,请设置 UNSLOTH_STUDIO_HOME:
$env:UNSLOTH_STUDIO_HOME="$PWD\.studio"; .\install.ps1 --local
$env:UNSLOTH_STUDIO_HOME="$PWD\.studio"; unsloth studio -p 8888
接下来进行更新:
cd unsloth; git pull
.\install.ps1 --local
unsloth studio -p 8888
高级启动选项
跳过 PyTorch(仅 GGUF 模式):
curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_NO_TORCH=1 sh
$env:UNSLOTH_NO_TORCH=1; irm https://unsloth.ai/install.ps1 | iex
跳过安装后启动 Unsloth 的提示(适用于自动化安装):
curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_SKIP_AUTOSTART=1 sh
$env:UNSLOTH_SKIP_AUTOSTART=1; irm https://unsloth.ai/install.ps1 | iex
固定 Python 版本:
curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_PYTHON=3.12 sh
$env:UNSLOTH_PYTHON='3.12'; irm https://unsloth.ai/install.ps1 | iex
使用 UNSLOTH_STUDIO_HOME 安装到自定义位置:
curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_STUDIO_HOME=/abs/path sh
$env:UNSLOTH_STUDIO_HOME='C:\path'; irm https://unsloth.ai/install.ps1 | iex
使用 UNSLOTH_NPM_REGISTRY 将前端构建指向企业 npm 镜像/代理:
UNSLOTH_NPM_REGISTRY=https://artifactory.example.com/api/npm/npm/ ./install.sh --local
$env:UNSLOTH_NPM_REGISTRY='https://artifactory.example.com/api/npm/npm/'; .\install.ps1 --local
在高核主机上限制 Unsloth 的原生 CPU 线程池:UNSLOTH_CPU_THREADS=8 unsloth studio -p 8888。
Vulkan、自定义 llama.cpp 后端:
可在安装时强制指定后端:
export UNSLOTH_LLAMA_CPP_BACKEND=vulkan # or cpu, cuda, rocm, auto
curl -fsSL https://unsloth.ai/install.sh | sh
$env:UNSLOTH_LLAMA_CPP_BACKEND="vulkan" # or cpu, cuda, rocm, auto
irm https://unsloth.ai/install.ps1 | iex
卸载
MacOS、WSL、Linux: curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.sh | sh
Windows(PowerShell): irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex
如需了解更多信息,请参阅我们的文档。
删除模型文件
你可以从模型搜索中的垃圾桶图标删除旧模型文件,也可以从默认 Hugging Face 缓存目录中移除相应的已缓存模型文件夹。默认情况下,HF 使用:
MacOS、Linux、WSL: ~/.cache/huggingface/hub/
Windows: %USERPROFILE%\.cache\huggingface\hub\
💚 社区与链接
| 类型 | 链接 |
|---|---|
| 加入 Discord 服务器 | |
| 加入 Reddit 社区 | |
| 📚 文档与维基 | 阅读我们的文档 |
| 在 X 上关注我们 | |
| 🔮 我们的模型 | Unsloth 目录 |
| ✍️ 博客 | 阅读我们的博客 |
引用
你可以按以下方式引用 Unsloth 仓库:
@software{unsloth,
author = {Daniel Han, Michael Han and Unsloth team},
title = {Unsloth},
url = {https://github.com/unslothai/unsloth},
year = {2023}
}
如果你使用 🦥Unsloth 训练过模型,不妨使用这张超酷贴纸! 
许可证
Unsloth 采用 Apache 2.0 与 AGPL-3.0 的双重许可模式。Unsloth 核心包继续采用 Apache 2.0,而某些可选组件,例如 Unsloth Studio UI,则采用开源许可 AGPL-3.0。
这一结构有助于支持 Unsloth 的持续开发,同时保持项目开源,并推动更广泛的生态不断发展。
致谢
- 感谢 llama.cpp 库,让用户能够使用 Unsloth 运行并保存模型
- 感谢 Hugging Face 团队及其 transformers 和 TRL 库
- 感谢 Pytorch 与 Torch AO 团队的贡献
- 感谢 NVIDIA 的 NeMo DataDesigner 库及其贡献
- 当然,也要感谢每一位为 Unsloth 作出贡献或使用过 Unsloth 的人!