unsloth/studio/frontend/tsconfig.test.json
Daniel Han d50f49e105
Ask for the transformers a training model needs, before the run starts (#8721)
* Ask for the transformers a training model needs, before the run starts

Muse Glimmer needs a transformers release no installed version ships. Chat
already handles this: /validate reports requires_transformers_upgrade, and the
composer offers to install the latest sidecar before loading. Training never
asked. Starting a Muse Glimmer run went straight to the loader and died there
with an architecture the installed transformers does not know.

Training now runs the same consent step chat does, before the custom code gate,
because installing a newer transformers changes what the load would run.

Two things the Configure preview got wrong and now discloses:

- A model no installed transformers ships reads as ready to train. It is not;
  the run stops on a consent dialog first.
- The latest sidecar loads 16-bit only, so a run that installs it cannot honour
  a 4-bit request. A "QLoRA, 4-bit" preview understated VRAM by roughly
  threefold. The preview now says 4-bit is unavailable for that model.

/validate grows forces_16bit so the frontend does not have to re-derive the
sidecar's rule, and resume runs go through the same gate as fresh ones.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Ask the upgrade gate about the model the run actually loads

Three things the gate got wrong.

A resume can be attested against a 4-bit model load that the latest sidecar
permanently refuses: effective_training_load_in_4bit raises
ExactResumeResourcesUnavailable for it the moment latest_tier_active_for turns
true, and that sidecar is a persistent overlay. Accepting Install from the new
resume gate therefore stranded a checkpoint that would otherwise have resumed.
The stored config cannot say so on its own (_sanitize_db_config strips
require_exact_resume_resources and require_exact_model_resource before the row
is written), so the resume names its run on /transformers-upgrade-check and the
backend recomputes the requirement from the provenance marker. With a
custom-code fallback the resume loads on the current runtime in the 4-bit mode
it needs, so no install is offered; without one the install is the only way the
run starts at all, and the dialog is raised as before.

The check was handed the Hub identifier for a cached model while the
remote-code gate and the worker both open the pinned snapshot, so a repo whose
current config.json names a different architecture than the snapshot on disk
was inspected instead of the model being loaded. The check now takes the same
four cache-pin fields as /models/remote-code-scan and resolves them by that
route's precedence, and each start path resolves the pin once for both gates.

A merely offered upgrade no longer claims 16-bit when the model can load
through its own repo code: the dialog offers that way out, taking it installs
nothing, and the run loads bnb 4-bit. /validate already exempts this case.

* Answer the upgrade gate for the load the run really performs

Three follow-ups on the training transformers-upgrade preflight.

Resolve a LoRA's base from the load target, not the Hub identifier. The
worker reads the base out of the adapter config it opens, and so does the
scan route; this route still read the repo's current one, so a pin taken
before the repo repointed base_model_name_or_path had the upgrade,
custom-code and precision answers describing a base the run never loads.

Stop offering an install that cannot rescue an exact 4-bit resume. Once the
sidecar is active, effective_training_load_in_4bit raises for exactly the
config the backend answered install_breaks_exact_resume with, so the resume
fails with or without the install and consent buys only a persistent overlay
that also retires 4-bit for later runs on that model. The start is now
refused with the reason instead. A model with custom code keeps the way out
it had.

Re-ask the Configure preview after an install. The notice cache outlived the
sidecar it described, so returning to Configure in the same session kept
offering a release that was already installed and kept previewing 4-bit for
a run the new overlay loads in 16-bit. The dialog store now counts completed
installs and the cache keys on that count.

Declining a dev-only upgrade no longer says to start the run again to
install it: that dialog has no install action, so the instruction could
never be carried out.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Let the test project see the ambient globals it now reaches

The training upgrade gate hands the chat feature a dynamic import, so the
tests that drive the gate pull src/features/chat into the test project for
the first time, and with it the Web Speech dictation adapter. That adapter
relies on the ambient declarations in src/speech-recognition.d.ts, which
nothing imports, so "include": ["tests"] never reached them and the
Typecheck step failed with five Cannot find name errors in a file this
branch does not touch. The app project only avoids it by including all of
src. Add the ambient declarations to the test project's include.

* Pin an implicit cache selection, and carry the custom-code verdict

Two more follow-ups on the training upgrade preflight.

Take the cache pin whenever prefer_local_cache is set, not only when a path
came with it. A cached inventory row can carry a null cachePath, and the Train
tab still sends the flag for it, so _resolve_model_snapshot has to search the
cache roots exactly as routes/models.py and /train/start already do with it.
Requiring a path left those selections judged on the repo's current
architecture while the worker loads the snapshot. The Configure preview now
resolves the same way, so the flag travels on its own rather than being read
back off the path, and the preview cache keys on it.

Carry the upgrade check's custom-code verdict into the custom-code gate.
confirmRemoteCodeIfNeeded falls back to the caller's requiresTrustRemoteCode
when the scan request itself fails, and the training callers were handing it
the stored config flag, which a fresh run leaves false. For a model that
needs its own modeling code the fallback then skipped consent and started a
worker with trust_remote_code off, which cannot load it. The check has
already answered that question, so the outcome now reports it and both start
paths pass it on, the way chat passes its validate result.

* Wait for the in-flight support snapshot instead of answering no upgrade

The Configure preview starts its own transformers-upgrade check the moment the
tab renders. On a cold snapshot that check fetches PyPI and the transformers
config mapping, and a user who presses Start before it lands sends a second,
concurrent check. _get_snapshot deduplicated those by handing the loser None,
which reads as "no upgrade needed" through latest_transformers_supports and
check_upgrade_for_model, so the start gate proceeded and the run launched on a
model no installed transformers can load.

The loser now waits on the running fetch and returns its snapshot, bounded at
20s so a request can never sit for a refresh's worst case; past that it falls
through to the same graceful answer a failed refresh gives. Still exactly one
fetch in flight.

* Answer the upgrade gate from the fetch, the release and the live generation

Four ways the gate still answered something other than what it knows.

The wait a concurrent caller makes for an in-flight support snapshot was a flat
20 seconds, but a refresh is five sequential URLs, each allowed one retry at the
five-second fetch timeout, so it can legitimately run for fifty. A wait that
expires mid-fetch answers None, and None reads as "no upgrade needed" all the
way up to Start, which launches the run on the architecture the gate exists to
stop. The bound is now derived from the refresh's own budget, so tuning a
timeout or a retry cannot silently reopen it.

The consent dialog offered "Continue with custom code" only once an install had
failed. Training raises that dialog before a run starts, so for a model that
ships its own modeling code the first dialog was Install or Cancel, and Install
activates the latest sidecar, which trains 16-bit. The 4-bit run the user asked
for had no way to start. The fallback is now offered next to Install whenever it
exists, out of one shared decision the dialog and its tests both read.

install_breaks_exact_resume is answered from the run's own provenance, without
regard to whether any release exists to install, so a resume on a dev-only
architecture was told that installing would strand it and to start a new run
instead: nothing can be installed, and a new run cannot load either. That branch
now applies only to an installable release, and the dev-only path explains the
real situation.

The Configure preview's notice cache keyed on the sidecar generation but let any
mismatch redefine it. A check fired before an install can still be in flight when
it lands and resolve after the post-install check has answered; its write rewound
the generation, cleared the fresh entry and stored the stale one, and since the
effect behind it was already cleaned up nothing re-rendered and nothing re-asked.
Superseded reads and writes are now ignored.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Bound the refresh transfer, and disclose the 16-bit Install

urlopen's timeout is a socket timeout: CPython documents it as "a timeout in
seconds for blocking operations like the connection attempt", so it bounds each
individual read, never the whole transfer. A response that dribbles bytes just
inside it therefore never expires -- measured here at 12.0s under timeout=5.0 --
which made every bound derived from that timeout an underestimate, the in-flight
wait included. A loser whose wait expired mid-refresh answered None, and None
reads as "no upgrade needed" all the way up to Start, so the run launched on the
architecture this gate exists to stop.

So the transfer gets its own wall-clock budget, checked between read1 calls,
and the per-attempt worst case is derived from that budget plus the one socket
read still blocking when it runs out. The in-flight wait is derived from the
attempt, and is now only a backstop: a waiter re-waits while that same refresh
is genuinely still running, since the winner clears the busy flag and sets the
event in one locked finally.

Separately, the Configure preview reported one precision for a run whose
precision depends on which action the user takes. A model that both ships its
own modeling code and is shipped by the offered release gets both dialog
actions: keeping the custom code loads it on the current transformers in 4-bit,
which is what forces_16bit answers for, while Install activates the latest
sidecar, which trains 16-bit. The card offered that install next to an
unqualified "QLoRA - 4-bit", understating the VRAM of the run by roughly
threefold if the user picked it. An offered install the backend does not already
call 16-bit is exactly that case and nothing else, so the card now names the
choice.

* Tighten the comments across the upgrade gate for PR #8721

Same explanations, fewer lines. The comments had grown to the point of
restating the code around them and repeating the same VRAM argument in
five places; each one now makes its point once.

Comments and docstrings only, verified per file with an AST comparison of
the code either side of the edit (24/24 unchanged). typecheck clean, ruff
clean, 37 frontend tests and 197 backend tests pass.

* Pin the compatibility guarantees for PR #8721 with regression tests

The existing tests prove the gate fires. These pin the opposite property,
which is what decides whether merging is safe: everything that worked
before it has to behave exactly as it did.

Backend, test_training_transformers_upgrade_gate.py:
  - an old client sends model_name alone, and a minimal response reads as
    the pre-gate behaviour, so a bundle newer than its backend still starts
  - forces_16bit over all eight combinations of sidecar, installable
    release and custom code, since the preview draws its VRAM claim from it
  - four failure types out of the preflights, none of which may escape a
    route that now runs in front of every start
  - the route stays behind authentication

Backend, test_transformers_latest.py, which chat reaches through /validate:
  - ordinary responses, plain and chunked, come back whole
  - a multi-chunk body is not truncated by the transfer budget
  - a 404 stays distinguishable from a failure, since auto_mappings.py does
    not exist on pre-5.10 tags
  - a truncated source fails the lookup rather than caching a short map that
    would offer an upgrade to every model missing from it

Frontend, training-transformers-upgrade.test.ts:
  - 404, 405, 500 and a network failure each leave the start untouched and
    raise no dialog
  - a partially populated upgrade is never offered as an install
  - a field from a newer backend is ignored rather than fatal

Measured while writing these: against a hostile mirror the snapshot refresh
returns in 20s where the derived bound is 150s, a second start after a
failure costs 0.000s behind the 300s backoff, and 50 offline checks take
0.1ms. The Configure notice and the consent dialog render identically in
Chromium, Firefox and WebKit with no console errors.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Key the sidecar guard on every target, and drop a redundant bool()

Two failures CI caught that the local suites did not, both introduced by
this branch and both passing on main.

The offline guard on latest_tier_active_for was keyed on the bare name
targets. The guard argument is what the call READS, and that function
resolves a remote adapter's base itself, so the whole tuple is the honest
key; the repo's other two call sites pass a tuple literal for the same
reason. tests/test_offline_gguf_cache_fallback.py asserts it.

requires_trust_remote_code was re-wrapped in bool() when constructing the
response. The value is already a bool, and the literal text reads as the
raw-YAML pattern the GGUF security-gate consistency test forbids. Dropped
there and on the two beside it.

218 gate/provenance/latest tests pass, ruff clean.

* Tighten the comments added with the regression tests

Final pass over what landed after the last one. Same points, fewer lines.

Comments only, AST-verified (3/3 code unchanged). 125 backend and 20
frontend tests pass, ruff clean.

* Drop an incomplete regex escape in the training upgrade test for PR #8721

---------

Co-authored-by: danielhanchen <unslothshared@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-14 04:59:56 -07:00

35 lines
1.2 KiB
JSON

{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
// vite/client so a test may import a src module that (transitively) reads
// import.meta.env; without it those reads fail to typecheck here only.
"types": ["node", "vite/client"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
// The ambient globals under src/ are declaration files, so nothing imports them and
// "tests" alone never reaches them. A test that (transitively) pulls in a src module
// relying on one -- the dictation adapter's SpeechRecognition globals, say -- would
// otherwise fail to typecheck here only; the app project sees them because it
// includes all of src.
"include": ["tests", "src/**/*.d.ts"]
}