Stacked on #14336. Merge only after the Linux torch 2.10 to 2.12
causal-conv1d / mamba-ssm cells from #14336 are published on
`prebuilt-wheels-cu13`; before that these URLs would 404 in front of the
upstream fallback.
## Why
Linux users on torch 2.10, 2.11 or 2.12 with a CUDA 13 torch download
upstream's causal-conv1d (205 MB) and mamba-ssm (351 MB) wheels, built
for torch 2.10 with `-lineinfo` and every arch. Our builds of the same
packages for torch 2.13 / 2.14 are about 29 / 54 MB on x86_64 and 42 /
79 MB on aarch64.
## Changes
- `_UNSLOTH_PREBUILT_EXTRA_CELLS` gains linux_x86_64 and linux_aarch64
torch 2.10 / 2.11 / 2.12 on cp311 to cp313.
- `_UNSLOTH_PREBUILT_EXTRA_CELL_PREFIXES` limits those Linux cells to
causal_conv1d and mamba_ssm, so flash-attn keeps upstream 2.8.1 there
(125 MB, smaller than ours).
- Unchanged: a CUDA 12 torch, cp310 and cp314 still resolve upstream,
and torch 2.13 / 2.14 and Windows resolve as before.
## Tests
Updated the tests that pinned upstream URLs for cu13 Linux torch 2.10 to
2.12. They now assert our wheel for cu13 cp311 to cp313 and upstream for
cu12 / cp310. Added tests that flash-attn stays upstream and that every
Linux extra cell the resolver asks for is one the workflow builds.
Affected suites: 1125 passed.
Follow up to #13861 and #13808. An audit of the merged on demand SSM
kernel change plus a T4 benchmark found a few gaps, fixed here.
## Why
- On a Kaggle T4 (sm75, torch 2.11 cu130, fp16, Qwen3.5-0.8B), Studio
chat decode was 4.4% slower without causal-conv1d (19.10 vs 19.95 tok/s,
6 alternating repeats, slower in all 9 pairs). Training speed, losses
and peak memory were unchanged. The zoo Triton conv covers
`causal_conv1d_fn` only, so single token decode fell back to the torch
update path. Training with trainable conv weights also allocates an fp32
dW scratch in the Triton backward, and packed `seq_idx` is not accepted
by the zoo dispatcher under transformers 5.20.
- Mamba-family models that have a working transformers torch path
(mamba, mamba2, falcon_mamba, jamba, zamba, zamba2, bamba) were made
fatal when mamba-ssm could not be installed.
- Export, local LoRA continuation, and inference model switching did not
provision or pick up kernels correctly once setup stopped installing
them.
- Setup on pre-Ampere GPUs (T4) downloaded flash-attn even though
Unsloth only enables FA2 at sm80+.
## Changes
1. Qwen3.5 / Qwen3.6 / Qwen3-Next request causal-conv1d on demand again
in training, inference and Clef (same as before #13861, minus the setup
time install). The zoo Triton platform probe is removed.
2. A missing mamba-ssm is fatal only for `nemotron_h`, `falcon_h1`,
`granitemoehybrid`. Other Mamba families install best effort and
continue on the torch path with a status message.
3. Local LoRA folders (`adapter_config.json`, no `config.json`) resolve
kernel needs from `base_model_name_or_path`; the training
`adapter_base_pin` wins when set.
4. The export worker provisions SSM kernels before importing Unsloth /
the export backend, with the same required vs optional split.
5. Inference re-enables the modeling source scan by reading
`transformers/models/<type>/modeling_*.py` as text, without importing
transformers.
6. If a kernel is newly installed after the model's
`transformers.models.<type>` module was already imported, the inference
worker asks for a restart and the orchestrator respawns it once.
7. Torch repair removes causal-conv1d / mamba-ssm whose compiled
extension no longer imports (never installs anything new). A hung import
check keeps the package.
8. Setup and the long context runtime install skip flash-attn below sm80
(and remove a stale copy at setup). No visible GPU and ROCm keep current
behavior.
## Tests
New and updated tests fail on main and pass here (`test_ssm_runtime.py`,
`test_ssm_restart_respawn.py`, `test_export_ssm_kernels.py`,
`test_clef_fast_paths.py`, `test_kernel_install.py`,
`test_training_worker_flash_attn.py`,
`test_ssm_kernels_install_python_stack.py`,
`test_flash_attn_install_python_stack.py`,
`test_windows_torch_flavor_invariant.py`). With
`test_install_python_stack.py` and `test_cuda_repair.py`: 966 passed
after merging current main.
## Hardening (second commit)
- Stale SSM kernel removal after torch repair needs proof: torch must
import first, and only an ImportError / OSError from the extension
itself removes the package. Signals, hangs, launch errors and a broken
torch keep it.
- `unsloth install-kernels --without-gpu` (or
`UNSLOTH_INSTALL_KERNELS_WITHOUT_GPU=1`) installs GPU gated kernels on a
GPU-less image builder. An already working kernel counts as success
before any skip, skipped kernels are listed, and the exit code is
unchanged (0) unless `--strict` is passed. The CLI respects `UV_OFFLINE`
/ HF offline variables and never falls back from uv to pip offline.
- Inference respawn refuses to spawn while the old worker survives
shutdown. SSM restart and the Xet fallback each get their own one-use
retry (3 spawns max).
- Restart detection only counts native `modeling_*` modules and the
selected checkpoint's remote code modules from its `auto_map`, matched
on the exact `transformers_modules` namespace.
- Export provisions SSM kernels inside the existing transformers
activation window (no extra Hub reachability probe), and skips
provisioning on macOS and for the MLX / CPU export backends.
- `UV_OFFLINE` counts as offline for on-demand installs (no URL probes,
source builds, or uv to pip fallback).
- Long context flash-attn install returns early when flash_attn already
imports, reuses the worker's own torch for the capability check, and
never applies the sm80 floor on ROCm.
## Install robustness (platform matrix review)
- A wheel is downloaded whenever its HEAD check is not a definite 404
(proxies that refuse HEAD no longer push required hybrids into a
failure), then import-verified; fallback only after a real failure.
- A failed install is remembered for the worker process (keyed by
kernel, torch, toolchain), so preflight and load share one attempt.
Optional Mamba families get mamba-ssm only as a prebuilt wheel, never a
source build. ROCm source builds get an 1800 s deadline and CUDA source
builds 7200 s (`UNSLOTH_SOURCE_BUILD_TIMEOUT` overrides); a timeout
kills the whole build tree.
- A runtime install whose extension does not import is uninstalled, so
transformers never finds a broken package.
- One cross-process lock per kernel and environment covers check,
install, verify and cleanup (training and runtime), with a recheck after
acquiring it.
- Only a CUDA or ROCm torch provisions anything; CPU, XPU, MPS and
unknown torch builds skip (export included).
- Optional mamba-ssm follows setup's sm80 floor (sm75 with Triton 3.4+);
the required hybrids keep the previous behavior.
- The resolver only returns URLs for cells that are actually published
(Unsloth release: torch 2.13/2.14 cp313, win_amd64 torch 2.10 to 2.12
cp311 to cp313; upstream causal-conv1d / mamba-ssm release cells), and
there is no source build of the pinned 1.6.1 / 2.3.1 sources on torch
2.13+, which need C++20.
- Training stays best effort for mamba-ssm, as before #13861.
- Export runs the malware and remote code consent gates before any
kernel provisioning.
- The extension is probed again after the install lock is acquired, so a
concurrent replacement is not treated as broken.
- The automatic HF offline window (Hub unreachable) no longer blocks
package installs; only user set offline variables do.
- `unsloth install-kernels --dry-run` prints the resolved URL while
offline.
- A Mamba model whose code needs mamba-ssm fails with a clear error on a
torch build without CUDA or ROCm.
## GPU checks (merge base vs head 4a6429a65)
Kaggle T4, torch 2.11.0+cu130, Qwen3.5-0.8B fp16:
| | base | head |
|---|---|---|
| causal-conv1d on demand | not installed | installed,
`causal_conv1d_update_kernel` engaged |
| decode tok/s (6 alternating repeats) | 18.70 | 19.41 (+3.8%, 6/6
pairs) |
| decode peak alloc | 1.748 GB | 1.748 GB |
| LoRA step time (3 repeats) | 1975.5 ms | 1970.7 ms (inside spread) |
| LoRA peak alloc | 2.338 GB | 2.338 GB |
| flash-attn at setup on sm75 | downloaded | skipped |
AMD gfx1151 (torch 2.11.0+rocm7.13.0): Qwen3.5 LoRA losses within 1.2e-4
relative (head vs merge commit bit identical), peak 2.389 GB both,
backend suites 262/262 and 275/275 passing.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* fix: align Python installer warnings with amber palette
* Test installer warnings use the shell installer warning color
---------
Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
* Studio: install prebuilt flash-attn, causal-conv1d and mamba-ssm wheels on Windows
Windows x64 resolves to our win_amd64 builds on prebuilt-wheels-cu13 (upstream
naming, cxx11abiFALSE, cp313, torch 2.13 / 2.14 cu13). Windows on ARM resolves
flash-attn to NVIDIA's Flash-Attention-for-WoA wheel for torch 2.14, pinned by a
#sha256= fragment that uv and pip both verify.
Windows is wheel-only everywhere: no upstream name is ever built (no upstream
release has a Windows asset) and a missing or rejected wheel never falls back to a
PyPI source build. Setup, long-context training, the causal-conv1d fast path and
its lazy hook, SSM inference and install-kernels all take the Windows path;
mamba-ssm on Windows first requires an importable triton. Linux URLs are unchanged.
* Windows wheels are cxx11abiTRUE: the Windows cu130 torch reports the flag as True
* Bound the Windows flash-attn setup probe like its import probe
* install-kernels: Windows mamba_ssm waits for Triton; rejected-wheel log says what Windows does
* Trim comments in the Windows kernel wheel install paths
* Re-check flash-attn when the Windows torch flavor invariant replaces torch
* Resolve the linux_aarch64 and win_arm64 prebuilt kernel wheels
* Studio installer: drop a flash-attn built for a torch that is gone
The Linux torch repair (step 13) runs after the flash-attn step. When it moves
torch, e.g. from 2.12+cu130 to 2.9+cu126 on a CUDA-12 driver, the cu13torch2.10
wheel the earlier step installed can no longer import. The repair re-selected
torchao and evicted a mismatched xFormers, but never looked at flash-attn.
Later passes did not heal it either: _ensure_flash_attn only removed a copy it
had just installed itself. With no wheel for the new torch (flash-attn v2.8.3
ships no cu12/torch2.9/cp313 asset) it warned and kept the broken one. xformers
imports flash_attn whenever find_spec sees it, so every diffusers pipeline then
failed with "libcudart.so.13: cannot open shared object file".
- Re-run _ensure_flash_attn when the step 13 repair moves torch.
- A resident flash-attn that does not import, under a torch that does, is
removed when no working wheel replaces it, and force-replaced when one does
(a same-version build for another CUDA otherwise counts as installed).
Fixes#13244 (flash-attn half).
* Studio installer: pair torch 2.9 with torchao 0.15.0
diffusers 0.41.0 imports torchao.quantization.FqnToConfig whenever torchao is
installed, and FqnToConfig first shipped in torchao 0.15.0. torch 2.9 hosts got
0.14.0, so once a broken flash-attn was out of the way, every diffusers pipeline
still failed to import with "cannot import name 'FqnToConfig'". transformers 5's
TorchAoConfig has the same 0.15 floor.
0.15.0 is also the build for this torch: its C++ extensions target torch 2.9.1,
while 0.14.0 targets 2.8.0 and skipped them on every 2.9 release. It is
published on every download.pytorch.org leaf that serves torch 2.9 (cu126,
cu128, cu129, cu130, rocm6.3, rocm6.4, xpu, cpu), which also closes the cu129
gap, where only 0.14.1 existed. torch <= 2.8 keeps 0.14.0.
Fixes#13244 (torchao half).
* Trim comments in PR scope
---------
Co-authored-by: mahiatlinux <mahiatlinux@users.noreply.github.com>
Anonymous GitHub API calls are limited to 60 an hour per IP. When that limit hit, the update banner, the llama.cpp changelog, release notes, the sd.cpp installer and the prebuilt wheel probes failed or misreported.
- A 403/429 that GitHub explains as throttling (429, Retry-After, X-RateLimit-Remaining: 0, or a rate-limit body) records one process-wide lockout until the reset, capped at an hour; a plain permission 403 records nothing. The freshness check, changelog and release notes honour it.
- Release notes send GITHUB_TOKEN / GH_TOKEN to https://api.github.com only, as an unredirected header.
- url_exists returns None when availability cannot be checked, so a refusal is no longer reported as "no published wheel"; callers still take their fallback path.
- The sd.cpp installer stops at the first rate-limited release lookup and says how to lift the limit, instead of trying every fallback repo.
- The llama.cpp installer resolves the diffusion visual server from the approved checksum manifest instead of listing release assets over the API.
* Collapse duplicated tool call parser tests into parametrized cases
* Parametrize transformers tier detection tests and share a config writer
* Parametrize sandbox tool policy allow and block cases
* Fold duplicated test cases into parametrized runs and shared setup helpers
* Hoist verbatim repeated test setup runs into shared helpers
* Drop the scratch area file list from the branch
* Name the generated parametrize arguments after what each value is
* Share the hardware verdict guard and fold the emission form loop tests
* Share the Responses stream drivers across the adapter tests
* Share the page fetch stub across the content type tests
* Restore the fuzz input labels and the finding tag lost when folding parametrized cases
* Name the folded parametrized tests and their arguments for what they check
* Restore the repo quote and keyword spacing style on the rewritten test lines
* Drop the dead return and its assignment from the tool output stream helper
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Re-record the patch targets the parametrized folds collapsed
* Preserve the full former test name in every folded parameter ID
The parametrize folds shortened or reworded some case IDs, so the pre-fold
test name could no longer be recovered from the node ID by grep. Restore it:
every folded case now carries the original test name minus its test_ prefix,
and originals that were themselves parametrized carry that name plus their
original case label.
IDs only. No test input, expected value or assertion is changed. Collected
node id count is unchanged at 57741.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Return only the routes module from the shared KB setup helper
_shared_setup_4 built its knowledge base inside `with _connection() as conn`
and then returned that connection. _connection is a `closing(...)`, so the
handle is already closed by the time the caller sees it. All four call sites
happen to shadow it with their own `with _connection() as conn` before any
use, which is why nothing fails today, but the helper is new here and handing
a closed connection to whoever writes the next test against it is a trap.
Return the routes module alone and unpack a single value at the call sites.
* Return only the paths from the shared archive and record setup helpers
_shared_setup_7 built its zip inside `with zipfile.ZipFile(archive, "w") as zf`
and returned that handle; _shared_setup_10 did the same with the file object
from `with open(...) as f`. Both are closed by the time the caller sees them.
Nothing fails today because the four zip call sites immediately shadow `zf`
with their own `with zipfile.ZipFile(archive) as zf`, and the three record
call sites never touch `f` at all. Both helpers are new here, so this is a
trap we would be creating rather than inheriting.
Return the archive path and the root directory alone, and unpack a single
value at the seven call sites.
* Trim comments in the folded test files
Drop the generated per-helper setup narration and collapse multi-line
case notes to one line where a single line carries the same fact.
Comments only; the parsed AST of every touched file is unchanged.
---------
Co-authored-by: danielhanchen <elliegouldingstuff@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
* Trim comments in tests, scripts and CI helpers
Comment-only pass over 453 changed files under tests/ (excluding the
studiobench harness), scripts/ and .github/scripts/.
17,749 -> 10,635 comment lines (-40.1%)
1,183,079 -> 757,190 comment characters (-36.0%)
Distinctive-fact retention against the base commit: 93.2%. A sample of 100
restoration and deletion decisions was judged by hand: 98 correct.
No comment line exceeds 120 physical characters. Nothing outside comments
changes: no code, docstrings, string literals, licence headers or pragmas.
Verified with an AST code-only signature of every file against the base.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Put the moved comments back on the code they describe
The signal-handler rationale in launch.py had been reattached to the
handler registration loop, which has no try/finally, and its except-branch
note had been split into a fragment starting mid-sentence with the noqa
marker duplicated into the prose. The PORT_FILE launcher comments had been
reattached to the _studio_root_id caching test, which does not touch
PORT_FILE.
* Put the section banners back, and use plain dashes in comments
The trim removed the rule-line comments that index these files. Tests locate a
region by finding its banner and end it on the next one, so a missing banner is
a broken anchor rather than lost decoration. Each is restored above the code
line it preceded at 1d2679f; where that anchor is not unique the banner is left
out rather than guessed at.
Em and en dashes are rewritten as plain dashes inside comment tokens only.
String literals are code and are untouched, so the diff stays comment-only.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Put reattached comments back on the code they describe
The comment pass moved a number of explanatory comments off the code they
were written for and onto unrelated statements nearby, so they now read as
false or as truncated fragments. This restores each one at its correct site:
- lint_duplicate_definitions: the import-key rationale goes back on the
emitted Finding key instead of being half-pasted onto `implicit =`.
- lockfile_supply_chain_audit: "Empty by default" belongs on the empty
CARGO_IOC_STRINGS tuple, not on the non-empty source allowlist; the
missing-lockfile and traceback notes go back on their own branches.
- scan_packages: "archives landed in dest" describes the successful pip
download, not `if not downloaded:`; the backslash-continuation note goes
back on the continuation check in _scan_line_end.
- interrupted_install_probe: the start_new_session/getpgid note belongs on
the posix branch that calls getpgid, not on the win32 branch.
- virustotal_scan: the missing-secret rationale goes back on the skip path
and the :⚠️: note back on the detection print.
- scan_npm_packages: the rmtree cleanup note goes back on the rmtree.
- check_frontend_dep_removal: one per-key rationale per key again, instead
of five concatenated onto browserslist.
- profile_startup: failed-launch and nothing-measured notes swapped back.
- enforce_kwargs_spacing: fold-rejection reasons back on the guards that
reject for them, and the f-string fold comment back in order.
- tests/conftest: the bitsandbytes broken-wheel note back on the
bitsandbytes handler.
Comments only; no code, string literal, docstring or pragma changes.
* Repair the banner restore: drop duplicate headings, keep the blank line
The scripted restore re-added each section banner without removing the
de-bannered plain heading the trim had left behind, so a heading appeared
twice, once ruled and once bare. It also inserted the banner flush against the
next line of code where the base had a blank line after it, which turns a
section separator into a doc comment for whichever def follows.
Both are the same fault the trim itself was being screened for, introduced by
the remediation rather than by the trim.
* Reattach 12 review comments to the code they describe
Each of these was a rationale that ended up on a neighbouring test or
assertion, or a fragment cut mid-sentence, so it described something
other than the line under it.
- test_gguf_disk_preflight: the 3GB-embeddings arithmetic belongs to the
untied case, not the tied one (2GB there).
- lockfile_supply_chain_audit: reassemble the workflow-command escaping
rationale on the skip branch.
- studio_smoke/multi_turn_chat: restore the retry-policy explanation in
main().
- test_virustotal_scan: restore the advisory-posture rationale on the
scan step.
- test_amd_fastpath_probe: return each ROCm rationale to its parameter
row.
- test_cuda_repair: the Blackwell span note goes back on the Blackwell
test; complete the Kepler sentence.
- test_windows_rocm_bnb_version: realign the sitecustomize / opt-out
rationales with their cases.
- test_dataset_num_proc: the Pool(1) note belongs to the low-memory auto
path.
- test_install_whisper_prebuilt_logic: decoy-DLL, libomp, build-bump and
same-ggml notes back on their own tests; the link copy fallback needs
none.
- playwright_loaded_models_indicator: stale-eject notes back on their
sections and the preference banner restored.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Restore the review comments that were cut mid-sentence or reattached to the wrong code
* Restore the comment tails the 120-column pass cut off
The trim enforced the column limit by chopping the physical line rather than
rewrapping it, so a block's last sentence stopped mid-thought and the clause
carrying the reason was gone:
-# ...fails in a second rather than
+# ...fails in a second rather than after the quantise and the multi-gigabyte save.
-# fp8 / mxfp8 need bf16 weights, so skip non-bf16 Linears;
+# fp8 / mxfp8 need bf16 weights, so skip non-bf16 Linears; nvfp4 handles fp32. Mirrors the runtime gate.
883 blocks across 266 files, restored from the merge base and rewrapped at word
boundaries within 120 columns. Blocks that end on proper punctuation followed by
a new sentence are left alone: those were shortened on purpose, and only a base
that continues the same sentence where the head stops counts as a cut. No word
is invented; every restored word comes from the base. Pragmas, shebangs, coding
lines, licence text and rule-line banners are skipped.
Dangling truncations drop from 336 to 77. Full suite is 65 failed / 18683
passed, matching main exactly, so no test changes behaviour.
* Restore comment tails cut inside a parenthetical or abbreviation
The first repair pass keyed on terminal punctuation, so a block cut after
"(e.g." or mid-code-span looked finished and was left alone. Balance the
brackets and backticks instead, and treat a trailing abbreviation as
mid-sentence:
-# ...skip the whole module up front on runners without it (e.g.
+# ...skip the whole module up front on runners without it (e.g. CPU-only) before importing unsloth, otherwise
+# collection errors instead of producing a skip.
22 further blocks across 20 files. _ts_deps.py is fixed by hand: it had also
lost a sentence from the MIDDLE of the block, so it was not a prefix of the
base text and no prefix rule could reach it.
* Reassemble the review comments that were left scrambled or cut short
Restore the full rationale for the comments the trim pass left as reversed
or truncated fragments, and put the openenv, reasoning-effort and recovery
window notes back on the code they describe.
* Put the scrambled comment blocks back over the code they describe
* Put the inline labels back on the entries they label
* Restore an inline label lost in the merge
* Restore the facts the reworded comments dropped
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Move the displaced notes back onto the tests they describe
* Re-pair the fixture rationales with their fixtures
* Put the relocated notes back and finish the sentences the trim cut
* Restore the dropped facts and correct the ones that were never true
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Unfuse the merged notes and drop the reformatting the trim smuggled in
* Restore the clauses the trim cut off and unfuse the notes it merged
Thirty sites where a comment was shortened past the point it still said
anything. Most ended on a colon or a dangling word, so the reason the
branch exists was gone while the sentence introducing it stayed. The
rest had absorbed a neighbouring test's note, which left one row
documented as another row's behaviour.
Each restored clause was checked against the code under it rather than
copied back on trust. Three were not restored: a pointer to a
measurement that is not in the function it names, a claim that a missing
import fails as NameError when it fails at collection, and a redundant
restatement in the autoscroll check.
tests/kaggle/t4_smoke/training_evidence.py ends up identical to main,
which is the right outcome for a file whose only trimmed sentence was
load-bearing.
* Give back the halves of the comments the trim cut off
Two hundred and twenty comments across a hundred and twenty files had
been shortened to the sentence that introduces a fact and none of the
fact. A note ending on a colon or on "so" or "carried over" still reads
as a comment and still parses, which is why every structural check has
been passing over them: a truncated comment is a well-formed comment.
Most of these were only findable by asking what a block ENDS on rather
than what it starts with. The seven passes before this one all keyed on
the first line, so they were blind to the commonest damage in the whole
trim. The supply-chain audit had seven "Mini Shai-Hulud May-12 wave:"
headers with the package names deleted, which was the entire content of
each one.
A handful were worse than truncated. A "can't fit on one line, so leave
it" note had moved from the test asserting changed is False onto the one
asserting changed is True; the sm_61 and sm_70 rationales in the CUDA
repair tests were stacked on a test that exercises neither, leaving
their own tests bare; an unpinned-latest note sat on a test that stubs a
working install on disk.
Restored clauses were checked against the code under them, not copied
back on trust. Around a dozen were left out or rewritten because the
base was wrong about the code as it stands: a pip stub credited with an
append it does not do, five exports said to be filled by a loop that
fills one, a __dict__ rationale that argues for the opposite of what the
loop needs, and UI strings quoted as current that the sources no longer
use.
* Put the fused row notes back on the rows they are true of
Ten sites where a comment described a sibling. The torchao skew table
had three rows' notes concatenated onto the 0.18.0+cu130 row; the
revision-forwarding table had the adapter-tokenizer exception one row
below the only row it holds for; the vision LoRA pair had the tied and
untied cases on a single anchor.
Four of these were mine, not the trim's. The 120-column rewrap left an
orphaned continuation line sitting on a class, one repair copied an
untagged-build note onto a fixture that pins a URL and has no untagged
build in it, another duplicated a namespace-package note and pushed the
interrupted-install label onto the wrong row, and a fourth moved a
collision note onto a fixture with a single host, where it is false. A
pass that rewrites 220 comments will get some of them wrong, and these
are the ones it got wrong.
Two reported sites were left alone. The masking note in the VirusTotal
client already carries its full clause, reflowed; the IOC scan comment
reads complete, and its missing "4." went with the numbering on every
other step in that function.
* Move the row and branch notes back onto what they describe
Nineteen sites. The token-count parity table had every row comment one
param too low, so each seeded reasoning style was documented as the next
one; the font-scale contract had two rationales slid down a continue
each; the validator fixture banner for R-INST-001 sat under R-INST-005,
leaving three tests unlabelled and two double-labelled.
Three of these I caused. The parity table was misplaced by the previous
commit, which moved fused notes onto rows without checking the seeds
underneath them; a timing bound landed in a test that has no timing
assertion; a restore note was reinserted between the two commands it
claims to cover instead of above them.
The pacing benchmark had lost its measurement table entirely, including
the #7892 result that is the reason there are two budgets rather than
one headline number. Restored with the columns intact.
* Undo the damage the last four repair passes did
Twenty-five sites, and almost none of them were the trim's. Round nine
was called "move the row and branch notes back onto what they describe",
and it moved a good many onto things they do not describe: the xpu swap
tests had the pinned-mirror rationale on the test that asserts the index
is dropped, the cuda repair file had a whole run of rationales slid one
test upward, and several notes were duplicated onto a neighbour rather
than moved. Where a clause was already correct on its real anchor, the
copy is deleted rather than parked somewhere new.
Separately, this PR had been changing code, and the gate could not see
it. Five files: three where a repair split a tuple across lines to make
room for a restored trailing comment, two where the trim deleted a
trailing comment and then joined up the wrapping that existed only to
carry it. Parentheses and line breaks leave the parse tree untouched, so
an AST comparison calls all of it comments. Every one is back to base
byte for byte, with the comments that justified the wrapping restored.
scripts/code_reflow.py is the check for it: same tokens in a different
arrangement is a code change, whatever the AST says.
* Let ruff rejoin the two lines the shorter comments freed
Both were split only to make room for a trailing comment. Round ten cut
those comments back to what is true of the row, the expressions now fit,
and the formatter joins them. Running it is not optional: pre-commit.ci
was red on exactly these two.
* Delete the stray copies rather than move them again
Five sites. Three were duplicates: the correct sentence still sits on
the test it describes, and a repair pass left a second copy on a
neighbour. Moving those would have been the fourth round of shuffling
one clause between two tests, so the copies are simply gone.
The mislabelling was the point in each case. An empty handover tag and
an absent one are different precedence rules, and the PowerShell 7.5
parsing note had been copied from the empty-string test onto the one
whose variable is unset. A non-zero-exit rationale sat on a parser test
that asserts a successful exit.
The other two are the trim's own: "#5854)," with its subject cut away,
and a regex description that stopped before naming the self. prefix it
matches, which is what explains the narrower filter below it.
* Put the nine slid notes back on what they assert
Seven had moved one branch or one call down. The autocast one had landed
on the case that forces float32 and asserts dtype float16, directly
under a note saying there is no dtype at all; the JAX rationale sat on
the USE_FLAX env-var case, whose module map has no jax in it; the
kill-and-sentinel note sat on a test that only checks a GPU row regex.
Each is back on the assert it describes, with a tenth found in the same
file as the probe timeouts.
Two are the trim's own. A mid-block deletion in the settings-tabs driver
spliced two half sentences into "blocking a / error-handling regression
and was a stale constant"; the macOS slot table lost the only statement
of why macos-14 is absent from a set named for images GitHub still
schedules. The retirement dates are restored, verified against all three
workflows that carry them.
The settings-tabs note is shorter than base on purpose. It now says the
invariant, that blocking a panel must not collapse the dialog, and drops
the tab count, which nothing asserts.
* Move twelve more notes back onto the branch they are about
All twelve had slid onto an adjacent test or branch, and in each the
displaced note contradicted what it now sat on. The CPU-backend note
about a wheel being left alone had landed on the test asserting a repair
runs; the XPU one about a dispatched shard sat on the single-device
case; the redirect-body note sat in a test that never binds served; the
draft-state refusal sat on the test where the target is present and the
run returns 0.
One lost text on the way as well: the missing-target rationale had been
cut to its first two sentences, dropping the reason the draft and
prerelease refusal above it would otherwise degrade into no check.
Also a rewrap that duplicated its own tail instead of replacing it, so
the merge-budget note read "else a sharded own device, not GPU0, else a
sharded model OOMs".
* Return six notes to the test whose behaviour they describe
Eight reports, six defects: two pairs described the same misplacement
from either side.
One was worse than misplaced. "A comment naming a probe is not a probe"
had moved onto the metadata scan, whose predicate matches the raw line
with no comment handling at all, so it claimed an exemption that scan
does not implement. It belongs on the Test-Path scan, the only one that
splits on # before matching. Moved, not reworded.
The rest argued against the test they sat on. The lazy-trl note was on
a stub with no __getattr__ to raise; the bogus-variant note was above a
request sending a valid variant; the imported-TensorFlow note was on
the case that supplies no tensorflow and asserts the flag is cleared;
the 62-token rationale was on the test that asserts 12.
* Move ten more notes onto the statement they explain
Seven had slid off their anchor, and in most the stray was also cut
short, so the fix was to restore the whole sentence where it is true and
delete the fragment. The torchao dependency reason sat on a test about
re-verification rather than on the --no-deps assertion it justifies; the
bitsandbytes heading sat on a test that fetches quantizers_utils.py; the
no-selection latch note sat on the delete block, which asserts nothing
reaches the action.
Two are the trim's own truncations. The stand-aside rationale in the
shortcut driver stopped at a colon, losing why Enter on a focused Deny
button would otherwise be defaulted away into approve. The kwargs-union
case stopped at a semicolon, and the trim had eaten the leading ** from
**kwargs as well.
One was about the driver rather than a test: the JSON note sat beside
the final print, where it described the document, instead of on the
stderr redirection that is what actually keeps stdout clean.
* Restore what the reflow cut, including three markers it mangled
Nine sites. Most are the trim's own truncations: the dill fixture had
lost the property that makes lookup by reference fail, so an otherwise
unexplained __module__ = "builtins" sat there with no reason; the rope
drift test had two clauses spliced into "every buffer keeps scaling in a
buffer"; the playwright guard had lost the three command shapes it
matches and why the regex is not anchored on an engine list.
Three are a shape I had not seen. The reflow rewrote the `#:` variable
documentation markers in the fast-copy constructs to `# :`, and dropped
stray colons into the wrapped prose, leaving "text : node". Those
markers are syntax, not punctuation to be tidied.
One was misplacement rather than truncation. The dotted-import rationale
had moved onto the second copy of its case and lost its tail, leaving a
dangling "and" above a capitalised "Still caught" and the first row bare.
Restored where base has it, stray deleted.
lint_duplicate_definitions self-test still passes, 63 cases.
* Put five drifted comments back on the code they describe
The trim slid several notes onto neighbouring tests, where they describe
something they no longer sit on:
- test_qat.py: the TODO moved off _test_full_model_fake_quantize, whose
leading underscore is what keeps pytest from collecting it, onto a test
that is collected and was never disabled.
- playwright_chat_ime_i18n.py: 6f carried a stray copy of 6e's
compositionstart note above an Enter KeyboardEvent, and had lost its own
rationale for why Enter reports isComposing=false while composingRef is
pinned.
- test_install_node_prebuilt_logic.py: the short-circuit note landed on the
transient-download-failure test, leaving the SHA-enforcement test bare and
dropping the carve-out that a different usable version is still kept for
offline resilience.
- test_chat_autoload_failure_gate.py: the default/namespace note moved onto
the named-import loop, and the mixed-form note explaining that regex's
optional prefix was dropped.
- test_frontend_dep_removal.py: the unquoted CSS url() note moved onto the
quoted fixture, and the HTML package-name-boundary note moved off the
script-src case onto the link-href case.
Comments only; the gate reports code unchanged on all five files.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
* Studio: verify the flash-attn import after installing it
A prebuilt wheel can install with exit code 0 and then fail to load, so the exit code
on its own is not proof the install is usable. Both Blackwell incidents were that
shape: in #5420 the older-arch wheels installed and raised on import, and the arch
gate added to work around it became the bug in #6961 once Dao-AILab started shipping
sm_100 wheels.
Verify the import after a zero exit code in the two paths that did not, and treat a
failure as not installed. ssm_runtime._install_kernel already did exactly this, for
the same reason, so this is that check applied to the setup installer and the
long-context training worker.
worker._is_importable() catches any exception rather than only ImportError: an
arch/ABI mismatch surfaces as OSError or RuntimeError ("undefined symbol"), which the
old pre-check would have let escape mid-training.
Also drop has_blackwell_gpu() and its two call sites. It has returned False
unconditionally since 03cbe21, so both blocks were already unreachable and removing
them changes no behaviour. An arch gate encodes a snapshot of what upstream publishes
and goes stale silently in both directions, whereas the import check catches a wheel
that will not load whatever the cause.
Checked on an 8x B200 host (compute_cap 10.0): the wheel this resolver builds for
cu13/torch2.10/cp313 carries sm_100 and sm_120 cubins, installs through
install_wheel(), and runs, matching SDPA to 0.0078 max abs difference on a
2x4096x16x128 bf16 causal forward.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Tighten the comments added by this PR (no behaviour change)
* Stub utils.native_tls in the MLX worker config test
worker.py imports six utils.* submodules and calls activate_native_tls() at import
time; _load_worker_module stubbed five of them. The file therefore loaded only when
another test had already imported the real utils.native_tls, so it passed in a full
run and failed on its own with "No module named 'utils.native_tls'".
Reproduced on upstream main at aaf99488, so this predates this branch: running the
file alone, or in any narrow selection, errors during collection. Cross-platform CI
runs the PR's own test files as a narrow selection, which is how it surfaced.
* Replace a rejected wheel on the fallback, and verify that install too
Rejecting a wheel that installed but would not import left the broken distribution in
site-packages, and the fallback installed over it rather than replacing it. pip reports
it as already satisfied and uv audits it as no change, both exiting 0, and that exit
code reached an unconditional return True. Long-context training then continued with an
unusable extension, which is the failure the wheel check was meant to stop.
Measured against a real flash_attn 2.8.1 install:
pip install --no-build-isolation --no-deps flash-attn
-> "Requirement already satisfied: flash-attn ... (2.8.1)", exit 0
uv pip install --no-build-isolation --no-deps flash-attn
-> "Audited 1 package", "Would make no changes", exit 0
So pass --force-reinstall (pip) / --reinstall (uv) on the fallback when a wheel was
rejected, and verify the import afterwards instead of trusting rc=0. The flag is gated on
the rejection so the ordinary path keeps installing over nothing and does not rebuild.
test_runtime_flash_attn_falls_back_to_pypi mocked an install that exits 0 without ever
making the module importable, which under the post-install check is a failed install, not
a successful one. It now flips the import stub when the install runs.
* Uninstall a rejected wheel instead of reinstalling over it, and probe out of process
Two problems with the previous commit.
--force-reinstall was the wrong tool. pip documents it as "Reinstall all packages even
if they are already up-to-date" and uv's --reinstall as "Reinstall all packages": both
scope to the whole resolved transaction, not the named one. flash-attn depends on torch,
so on the plain fallback path (which carries no --no-deps) that could reinstall or
downgrade the torch the worker is currently running on. Measured with a stand-in whose
metadata resolves, since flash-attn's sdist cannot build metadata here:
pip install --dry-run requests -> 0 packages
pip install --force-reinstall --dry-run requests -> 5 packages
(requests + its whole closure)
It also fails outright on that path: --force-reinstall makes pip rebuild flash-attn from
sdist, and the plain branch passes no --no-build-isolation, so the build dies on
"No module named 'torch'". This file already pairs --force-reinstall with --no-deps
elsewhere for the same reason.
So remove the rejected distribution and leave the fallback command untouched. Nothing is
reported as already satisfied afterwards, the transaction stays exactly as wide as it was
before, and no rebuild flags change.
Second, the post-install probe now runs in a child. A wheel built for the wrong arch can
abort or segfault inside the extension's initialiser rather than raising, and except
Exception cannot catch that: it would kill the worker and take the fallback with it. The
child turns it into a return code, negative for a fatal signal. install_python_stack
already probes this way; the worker now matches it. The cheap in-process check stays for
"is it already installed", which costs no spawn on every training start and imports the
module the worker wants imported anyway.
The module name is passed as argv rather than formatted into the -c body.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Bound the installer's flash-attn import probe
The worker bounds this same untrusted import at 300s and handles TimeoutExpired; the
installer probe had no timeout, so a native extension that hangs in its initialiser
rather than failing would leave setup waiting forever and never reach the warning. The
asymmetry arrived with the post-install probe added earlier in this branch, which is
exactly the call that hands an unvalidated wheel to the import machinery.
Bound it the same way and treat a hang, or a probe that cannot be spawned, as an
unusable wheel. The timeout is a named constant on both sides so the two paths cannot
drift apart again silently.
* Tighten the comments added in review (no behaviour change)
* Remove a rejected flash-attn wheel in the installer too
The setup path warned and continued while leaving the unusable distribution in
site-packages, so "Continuing without flash-attn" was not true. unsloth/models/_utils.py
gates on _package_available("flash_attn"), which reads metadata rather than importing,
and then does an in-process "from flash_attn.flash_attn_interface import ...". A wheel
that aborts during native initialisation therefore takes the training process down, and
that is exactly the wheel this branch has just rejected.
Confirmed with a package that calls os.abort() at import: importing it in process kills
the interpreter with SIGABRT, while the out-of-process probe returns rc -6 and survives.
Uninstalling a properly installed distribution removes it cleanly.
The worker path already uninstalls before its fallback; the installer now matches.
* Uninstall with the mode uv was installed with, and stop claiming a failed removal
Two problems in the cleanup added in the previous commit.
It hard-coded "uv pip uninstall --python". _bootstrap_uv sets UV_NEEDS_SYSTEM exactly
when the --python probe FAILED and --system succeeded, so on those hosts the cleanup used
the one mode already known not to work there: the uninstall fails, the unusable wheel
stays in site-packages, and unsloth/models/_utils.py still finds it by metadata and
imports it in process. Mirror the install mode instead. uv documents --system as "Use the
system Python to uninstall packages".
It also printed "removed it" unconditionally, so a failed removal produced two
contradicting warnings in the same run: "Could not remove the unusable flash-attn
install" followed by "...; removed it". The helper now returns whether the package is
actually gone, and the caller says which happened. A wheel still installed is not the
same state as never having installed one, so the failure case says so and points at the
manual uninstall.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Route every rejected install through one discard path
The PyPI fallback returned False without removing the distribution it had just rejected,
so the unusable extension stayed where unsloth/models/_utils.py finds it: that gate reads
package METADATA and then imports the native module in process, which is exactly the
import the isolated probe could not survive. Two ways in: a failed uninstall leaves the
wheel "already satisfied" so the fallback no-ops, or the source build itself produces an
incompatible extension.
This is the third round on the same defect, in a third place, so fix the shape rather
than the instance. _reject_install is now the single discard path: it uninstalls, and it
reports which state we actually ended in. _uninstall_package returns whether the
distribution is gone rather than only logging.
The pre-fallback uninstall stays a plain call: a failure there is not fatal, because the
fallback then no-ops on "already satisfied", the probe rejects it, and _reject_install
reports the real state.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Enforce the discard invariant in one place instead of at each return
The timeout and install-failure exits returned without discarding the rejected
distribution, so a wheel that had already failed its import check stayed where
unsloth/models/_utils.py finds it: that gate reads METADATA and only then imports the
native module in process. The comment added last round claimed the fallback path would
clean up on its own, and that is only true when the fallback exits 0.
This is the fourth defect of the same shape, each one an exit somebody did not think to
clean up, so enforce the invariant structurally rather than adding a fourth call.
_install_package_wheel_first now keeps the two "touch nothing" guards (already importable,
offline) and delegates the rest to _attempt_package_install, discarding whatever is left
in a finally. Any unsuccessful exit, including ones added later, is covered.
The discard is state-based so it is safe to run everywhere: _distribution_present reads
metadata via importlib.metadata without importing, so it never loads an extension that
would abort, and _reject_install no-ops when there is nothing installed.
Verified on the B200 that a working flash-attn 2.8.1 is still untouched: three consecutive
runs return True with zero install or uninstall subprocesses, and it stays importable.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* State the installed-but-broken state in the rejection tests
Two rejection tests asserted on the discard status message without pinning
_distribution_present, so after the discard became state-based they only passed where
flash-attn happened to be installed. That was true in the venv I had been running them in
and false on a clean runner, where the discard correctly no-ops and no status is sent.
Cross-platform CI caught it on ubuntu-latest; the same two tests fail in a fresh venv here
and pass with the state pinned.
No production change: the invariant holds either way. The tests were reading the machine
rather than the code.
* Uninstall from the interpreter install_wheel actually installed into
install_wheel always targets sys.executable: its uv command passes --python in addition
to --system, and its pip fallback runs that interpreter directly. The cleanup passed
--system INSTEAD of --python, so on a UV_NEEDS_SYSTEM host it uninstalled from the system
Python while the wheel sat in the venv, and setup then reported the wheel removed when it
was still there for the metadata gate to import.
Pass both, so the removal mirrors the install rather than half of it. The earlier test
asserting the --system-only command is replaced, since it pinned the broken command.
The worker path already targets --python sys.executable on both sides and needs no change.
* Tighten the review comments (no behaviour change)
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* fix: Remove moot has_blackwell_gpu() function
Fixes unslothai/unsloth#6961. This function skipped flash-attn on Blackwell GPUs because no prebuilt wheel existed;
Dao-AILab now ships one and url_exists() already gates resolution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: use torchao 0.17.0 for Blackwell
Fixes#6961. Torchao 0.16.0's cpp extensions are built against CUDA 12, so on a CUDA-13
torch (cu130 / Blackwell) they fail to load with "libcudart.so.12: cannot
open shared object file". Select 0.17.0 there instead: its cpp targets torch
2.11, so it is skipped cleanly rather than crashing. CUDA-12 / ROCm / CPU
torch 2.10 keeps 0.16.0 and its working kernels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Condense torchao version-selection comments (no behavior change)
* Support torch 2.11 in the Studio installer via the torch2.10 prebuilt wheels
Map torch 2.11 to the torch2.10 prebuilt wheels for flash-attn, causal-conv1d,
and mamba through wheel_utils.prebuilt_wheel_torch_mm, applied in direct_wheel_url
(filename) and flash_attn_wheel_url (version). Those torch2.10 CUDA wheels load and
pass each project's own test suite on torch 2.11 (verified on B200), so a torch 2.11
environment gets the prebuilt accelerators instead of skipping or building from source.
Raise _CUDA_TORCH_PKG_SPEC to <2.12.0 (torchvision <0.27.0, torchaudio <2.12.0) so
the CUDA torch repair path can install torch 2.11, where torchao 0.17's cpp kernels
load cleanly. Add tests for the mapping.
* Keep has_blackwell_gpu as a False stub for future arch gating
* Restore has_blackwell_gpu as a return-False probe kept for future arch gating
Keep the nvidia-smi compute_cap detection and its two call sites, but short-circuit
with return False at the top so flash-attn is no longer skipped on Blackwell (sm_100+
now has prebuilt wheels and url_exists gates resolution). Drop the early return to
re-enable arch-based detection later.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Raise ruff line-length to 100 and extend the local pre-commit format pipeline (def-signature magic-comma normalization, short multi-line assert collapse, kwarg '=' spacing, blank-line-after-short-import removal, adjacent string-literal / f-string+plain merge, redundant-pass pruning). Every transform re-checks the file AST and is dropped if it would differ; the whole-repo reformat is verified AST-identical per file and idempotent.
* studio: skip flash-attn install on Blackwell GPUs (sm_100+)
Dao-AILab does not publish prebuilt flash-attn wheels for sm_100, sm_120,
or sm_121, and the older-arch wheels fail to load on Blackwell. Add a
shared has_blackwell_gpu() helper and gate both the install-time
(install_python_stack._ensure_flash_attn) and runtime
(worker._ensure_flash_attn_for_long_context) paths on it. Detection uses
nvidia-smi --query-gpu=compute_cap, which works on Linux and Windows.
* test: stub has_blackwell_gpu in pre-existing runtime flash-attn tests
prefers_prebuilt_wheel and falls_back_to_pypi exercise the install
paths that the Blackwell guard now short-circuits. Make them explicit
about non-Blackwell so they pass on real Blackwell hosts.
* studio: cache has_blackwell_gpu, skip Blackwell warning under NO_TORCH
- Wrap has_blackwell_gpu in functools.lru_cache so repeated calls in a
single process avoid redundant nvidia-smi spawns. Tests clear the
cache via setup_method/teardown_method.
- In _ensure_flash_attn, run the NO_TORCH short-circuit before the
Blackwell check so GGUF-only users (who never install torch anyway)
do not see a Blackwell warning. Blackwell check still runs above the
IS_WINDOWS / IS_MACOS gates so Blackwell-on-Windows users still see
the explicit reason rather than a silent OS skip.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* test: add has_blackwell_gpu to mlx worker test wheel_utils stub
test_mlx_training_worker_config loads worker.py against a hand-rolled
utils.wheel_utils stub. Adding has_blackwell_gpu to the stub symbol
list so worker's import line resolves.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* [Studio] Install flash attn at setup time for linux
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* cleanup changes
Signed-off-by: Datta Nimmaturi <venkatadattasainimmaturi@gmail.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Test cases
* wheel_utils: narrow url_exists exceptions and log at debug level
---------
Signed-off-by: Datta Nimmaturi <venkatadattasainimmaturi@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Roland Tannous <rolandtannous@gravityq.ai>