mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-12 18:34:19 +00:00
* feat(studio): rework training setup and cache handling * Rework train page resource selection * Improve train page workflows and resource selection * Fix train page picker behavior * Fix stale training errors after token updates * Fix training eval steps regression * Fix shared token use when resuming training * Harden train page resource workflows Unify model and dataset picker behavior, training start guards, and token handling. Validate cache provenance for local models, downloaded resources, and processed datasets. Prevent stale preview and history state while keeping the train page modular. * Fix training method race and test isolation * Restore training upload limit exports * Fix training config formatting contract * fix(studio): harden training model selection and preflight * fix training resume, cache, and lifecycle reliability * Fix training stop watchdog path stub * Fix training start state and train page UI * Unify train page selection controls * Fix train page validation, localization, and paging * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Clean up train locales and restore recipes link * fix(studio): correct training model selection and preflight Preserve freeform local model paths, reject remote GGUF-only repositories, and correct the training start snapshot contract. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): harden training resource selection Preserve local dataset path intent, localize setup-change errors, and remove unused picker tour hooks. Refresh model picker and PDF recipe contracts for the refactored training flow. * fix(studio): remove train tab scrolling * fix(studio): harden training resource preflight Reject missing local models and binary adapter artifacts. Preserve selected model and dataset cache pins through preflight and QLoRA loading. Detect cache path changes before training starts. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix train page validation and cache handling Keep transient model checks nonblocking and bind cached models and datasets to their selected snapshots. Improve Hub auth errors, retries, task search, streaming consistency, locale formatting, reset handling, payload mapping, partial inventory filtering, theme accents, and regression coverage. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): stabilize train page pickers Unify Hub validation, local inventory, picker styling, and dataset names. Localize training feedback and explain streaming modality changes. Add unit, contract, and cross-browser picker coverage. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): align cached scans and variant state purge Scan selected cached snapshots during training security preflight. Purge manifests and cancel markers using their stored GGUF variant. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): harden train cache, consent, and UI Fix cache cleanup and cross-platform model path handling. Align resume and adapter consent scans with pinned load targets. Localize dataset flows and refine navigation, state cleanup, and theming. * fix(studio): harden training resource selection Preserve cached model configuration HTTP errors so stale cache references return their intended 404 responses. Wait for device inventory settlement before locking inferred picker tabs, while keeping known device items visible during scans and retries. Apply modality name heuristics only to the final model path component across supported platforms. Expose full model and dataset identities on truncated picker triggers. Add regression coverage for picker settlement, retry behavior, and modality inference. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix train picker flow and resume safety Scan the exact cached model snapshot before resume consent and preserve the actual repository identity. Keep cold pickers on Device until inventory settles, cap automatic Hub pagination, and provide a stable Load more action. Improve train control and history card accessibility, remove duplicate token labeling and dead styling, and correct picker spacing. Normalize Windows relative model paths and add coverage for resume pins, picker policy, pagination, path identity, and accessibility contracts. Fix the reported formatting and import ordering issues. * Fix cross-platform train dataset path detection Use the shared local path detector so persisted Windows, Unix, UNC, and home-relative dataset references do not render Hugging Face-only controls. Recognize Arrow dataset files and add regression coverage for supported local path formats and Hub repository identifiers. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix training start recovery and dataset selection Reconcile failed fresh and resumed training starts with backend status before reporting an error. Treat the selected dataset source as authoritative so filename-like Hub repositories retain subset and split controls. Align dataset picker sorting and remove unused advisor template state with a persisted-state migration. Add regression coverage for transport recovery and Hub dataset selection. * Fix train picker validation and start recovery Update the resume training contract test for the asynchronous failure handler. Filter invalid Hugging Face search results and block invalid saved model and dataset selections. Restrict local dataset selection to inventoried application paths. Load training YAML through a bounded native file picker while keeping the browser fallback. Only recover uncertain training starts after network or response parsing failures, and match the active job to its request identifier. Treat cached full precision models as ready for QLoRA without showing a false download warning. Add regression coverage for the picker, config import, and training recovery behavior. * Harden training starts and config imports Track training start request IDs from preflight through pending, accepted, and rejected states so retries remain idempotent. Disable automatic retries for start posts and reconcile ambiguous client responses with server request status. Clear terminal training status on reset so later runs do not inherit stale state. Enforce the 1 MiB YAML limit for browser imports and keep read errors specific to the selected file label. Remove unused dataset picker translation entries across all locales. Add focused tests for request idempotency, start recovery, config limits, and picker contracts. * Harden train picker feedback and start handling Show explicit empty states for device dataset searches and invalid Hub model or dataset queries while keeping pagination sentinels mounted. Run the train picker Playwright suite across Chromium, Firefox, and WebKit in Windows CI. Keep training start outcomes owned by backend spawn completion so cancelled handlers cannot mark active jobs as rejected. Correct the Train section heading hierarchy and format the branch-owned native file dialog test. Add picker contracts, browser assertions, and cancellation race coverage for the updated behavior. * Fix training setup races and configuration state Keep hardware-based method selection within model-default loading so starts cannot race changing defaults. Clear start errors only after deliberate configuration or token edits while background reconciliation stays silent. Generate training request IDs safely for LAN HTTP access before acquiring the runtime lease. Filter persisted state, sanitize invalid methods, and guard preview metadata. Localize config size failures, use the toast wrapper, and remove obsolete training files. Add regression coverage for the updated start, persistence, and import behavior. * Refine training configuration and picker workflows Split the training configuration store and parameter panel into focused persistence, policy, LoRA, hyperparameter, memory, and MLX modules. Run picker coverage on platform-appropriate browser engines and parameterize the unmanaged dataset path. Surface configuration changes when streaming is disabled and clear stale dataset modality state after failed probes. Consolidate dataset format and AI-assisted mapping on hub routes with header-based Hugging Face tokens. Hold local path actions until model inventory loading settles. * Fix train picker state and preflight regressions Open online pickers on the Hub before inventory settles and keep the inferred tab stable for the session. Preserve parameter and VRAM metadata for pinned selected models. Reject failed backend starts once and cancel automatic modality corrections without showing a destructive setup error. Update persistence contracts to follow the extracted training configuration module. Move shared parameter option styling out of the React component module to preserve Fast Refresh. Add regression coverage for picker tab behavior, pinned model metadata, persistence contracts, and start rejection handling. * fix(train): harden model selection and start feedback Preserve cache metadata and model capabilities when freeform local paths resolve to discovered models. Return stable training start error codes through direct and recovered requests, then localize Hugging Face preflight failures across every supported locale. Show whether submitted advanced settings are default or non-default in Simple mode, and migrate the parameter mode preference to the standard storage key. Restore absent-key migration semantics and update the embedding security test for the extracted gate helper. Add focused coverage for structured errors, advanced setting summaries, start recovery, and security gates. * fix(train): harden picker validation and focus Align Hub ID validation with Hugging Face rules while accepting valid underscore boundaries. Reject persisted path-shaped datasets from Hugging Face-specific controls. Reuse model identity normalization for cache path comparisons. Move picker search focus into the popover focus lifecycle. Update source contract tests for extracted model selection and structured resume errors. * fix(train): refine picker behavior and dataset structure Wait for device inventory before locking inferred picker tabs while preserving explicit user choices. Align Hub resource ID validation with backend rules for leading and trailing underscores. Support ArrowDown navigation from picker tabs into available options. Split dataset selection, uploads, streaming settings, and inventory refresh logic into focused modules. * fix(studio): stabilize train picker contracts and state Update dataset contracts to follow the extracted selection, upload, and inventory modules. Keep the inferred picker tab stable while device inventory settles. Use the shared TrainingMethod type for VRAM estimation and remove redundant casts. * fix(train): align Hugging Face repo validation Allow underscores at repository segment boundaries to match Hugging Face repo ID rules. Keep model and dataset search queries unvalidated while preserving selection safeguards. Add regression coverage for accepted IDs and unrestricted picker searches. * Fix training resource compatibility and cache safety Restore legacy dataset format and mapping routes while moving training uploads and checks to the canonical Hub endpoints. Enforce configured upload limits, clean partial files after failures or cancellation, and support both upload route families in middleware. Align Hugging Face repository validation with accepted underscore boundaries and skip PyTorch dependent tests when the dependency is unavailable. Keep model format and cache metadata correct when the same repository is selected from another source. Only promote complete runnable model weights as cached training resources, align download notices with picker capabilities, and reject weightless local snapshots during start preflight. Cancel stale dataset mapping assistant requests before they can overwrite a newer selection. Preserve touch selection for training methods by separating tooltip toggling from Select item activation. Add focused backend, frontend, and picker regression coverage for these paths. * fix(studio): finish train page and onboarding rework Ignore cache-only fields when comparing training start inputs so reconciliation does not cancel valid runs. Route native dataset drops through Tauri path validation and lease-backed managed uploads. Use the production model and dataset selectors in onboarding and replace placeholder uploads with real imports. Require positive learning rates while keeping learning rate, epoch, and step inputs editable. Normalize model identities, improve dataset summary order, and guard persisted selection state. Recognize Windows rooted and drive-relative paths consistently in picker logic. Add MLX optimizer help and learning rate validation messages to every supported locale. Add frontend, backend, and native policy regression coverage for the updated behavior. * chore(ci): remove train picker smoke workflow changes Remove the train picker Playwright step from the macOS Studio smoke checks. Remove the train picker Playwright step from the Linux Studio smoke checks. Remove the train picker Playwright step from the Windows Studio smoke checks. Drop the matching artifact upload paths so the workflow files match upstream main. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): harden train picker behavior and boundaries Expose dataset drop helpers through the training public API and move local path detection into shared code. Block freeform Hub choices while offline for pointer and keyboard selection. Make native dataset drop handling safe for committed React renders. Keep recipe navigation working when session storage is unavailable. Preserve Windows drive root identities and cover the distinction from drive relative paths. Update picker and training input contracts to match the extracted implementations. Remove orphaned translations and align the train style block with the existing indentation. * Fix train picker cache and upload behavior Keep selected model cache paths strict and preserve fallback discovery only when no path is supplied. Offload multipart writes and native dataset copies from the backend event loop. Prewarm train picker inventories so device-first tab inference can settle before the first open. Use shared picker tab constants across the model and dataset selectors. Update picker contracts and add regression coverage for cache resolution and dataset uploads. * Refine train picker controls and component structure Split picker retry, error, and pagination UI into focused components. Add a shared segmented control and use native radio semantics for dataset source selection. Centralize segmented indicator positioning with RTL-aware transforms. Separate dataset advanced settings state wiring from its presentation and source toggle. Make picker arrow navigation move symmetrically from tabs through search and options. * Fix train page reconciliation and picker contracts Preserve tuned training parameters when model cache references change, while refreshing defaults when no user edits occurred. Keep streaming modality corrections visible through the Start CTA after preflight stops. Localize the new training parameter and dataset mapping text across all supported locales. Refresh picker structural contracts after the component split and apply Biome formatting to the changed files. * Fix train upload tests and desktop drop handling Pass an explicit empty native path lease in legacy upload limit tests so direct route calls match FastAPI request behavior. Track Tauri scale factor changes during dataset drag and drop, clean up both listeners safely, and cover runtime updates. Replace the run preview card's important border utility with an explicit dark theme card modifier. * fix(studio): harden training setup and history state Restore Hugging Face token entry and validation in the onboarding model and dataset steps. Apply the configured upload size preflight before onboarding dataset uploads. Update moved model and dataset cache references without showing false missing cache warnings. Parse SSE frame delimiters by their matched length so mixed line endings preserve event data. Match sensitive pid paths by exact segment while retaining database state protections. Clear deleted Data Recipe selections after inventory settlement while preserving direct uploads. Show the files deleted status only when a run previously recorded an output directory. Handle Data Recipe navigation failures and keep the native drop listener stable during uploads. Replace formatting-specific contract checks and add focused behavioral regression coverage. * Harden training start and model picker flows Split the train model picker into its own entry point to keep it out of shared route bundles. Preserve pending idempotent start reservations and reconcile them before accepting a run. Keep unconfirmed starts in a polling state with accurate localized feedback. Continue starts after automatically disabling unsupported multimodal dataset streaming. Localize the updated model picker controls across supported languages. Lock dataset uploads before asynchronous preflight to prevent concurrent selections. Add regression coverage for reservation states and unconfirmed runtime starts. * fix(studio): harden training picker edge cases Preserve POSIX model path identity while retaining Windows relative path normalization, and clarify the override migration behavior. Localize the onboarding model and dataset controls across every supported locale. Truncate native dataset filenames by Unicode code point so generated labels cannot contain lone surrogates. Cancel training after preflight disables streaming for image or audio datasets so users can review the changed setting before restarting. Update runtime contract assertions for the setStartPending rename and add regression coverage for path identity and Unicode truncation. * fix(studio): align training picker controls Restore full-height Browse and Amazon S3 selection with an accessible radiogroup that avoids fieldset sizing behavior. Align model and dataset picker tabs with the shared 36 px segmented control geometry and typography. Remove active borders and focus rings from picker search inputs while preserving keyboard focus indicators on other controls. Add contract coverage for segmented control sizing and picker search focus styling. * fix(studio): harden onboarding picker flows Classify native dataset drops from the full path before shortening display names. Keep onboarding model choices within the selected training type across Hub, device, and freeform selections. Retry model default loading when onboarding restores an interrupted model selection. Localize the Hugging Face token field across supported languages. Reset picker result scrolling when switching between Device and Hugging Face tabs. Add regression coverage for native filenames, model constraints, hydration, and picker scrolling. * fix(studio): reconcile multimodal streaming before training Disable streaming when dataset checks detect image or audio data, then recheck cached selections in non-streaming mode. Keep start-time modality detection as a safe fallback that updates the configuration and continues without an error or a second click. Build onboarding training method options from the shared method order and metadata so onboarding stays aligned with the train page. Add contract coverage for the streaming reconciliation and shared training method list. * Fix train picker contracts and dataset recovery Allow local models without reliable modality metadata to pass onboarding constraints while retaining explicit mismatch checks. Reconcile GPU selection without effect-driven state updates and mark intentional deep Hub imports for lint. Return and consume stable local dataset cache miss codes, and send the canonical train_split field. Move train-specific picker code into its owning feature and update the related contract coverage. Use the canonical Hub dataset progress route from Chat. * Harden training resource selection and offline handling Return an actionable preflight error when offline mode is enabled and the selected model is not cached. Validate evaluation dataset uploads with the shared extension policy and reuse the centralized accept list. Disable streaming for explicit on-device dataset selections while preserving Hugging Face selection intent. Keep dataset identifiers safety checked without blocking benign values before Hugging Face handles repository validation. Remove the branch-added source assertions and cover the new selection policy with behavioral tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix training setup validation and async state races Validate Hugging Face dataset IDs and repository access before training starts while keeping local paths on their filesystem validation flow. Scope model default race protection to fields that defaults can overwrite so unrelated dataset edits no longer suppress model initialization. Stop Hugging Face token edits from invalidating token-independent device inventory scans on every keystroke. Add regression coverage for dataset source validation, cached and remote Hub preflight, model default application, and token inventory behavior. * Tidy up train page layout, tooltips and dark mode Layout - Move the Browse / Amazon S3 toggle into the Dataset section header - Put Subset, Train Split and Evaluation Split on one row - Put Target Format, Train Split Start and Train Split End on one row - Pair Project Name with Max Steps, and Context Length with Learning Rate - Centre the hyperparameter tabs and give them a fixed width so the sliding indicator lines up with its segment - Give parameter rows a min height so slider and select rows share one vertical rhythm - Slightly more spacing below section headings and at card bottoms Tooltips - Add hints for Model, Method, Dataset and Project Name - Move field descriptions into the tooltips and drop the inline copy - Upload field keeps the accepted file types inline, with size limit and Learning Recipes note in the tooltip HF token - Show a masked preview of a saved token instead of a generic label - Read Not set when no token is stored Dark mode - Drop borders on the LoRA option cards and target module chips, and separate states with background fill instead - Use a lighter, less saturated green for the selected state Other - Expand LoRA Settings and Training Hyperparameters when Advanced opens - Shorten the upload label to Drop file or click to upload - Localise the new wizard tooltips across all locales * Fix training defaults, MLX validation, and dataset preflight Prevent late hardware recommendations from replacing configuration values loaded after model selection. Block unsupported MLX methods and embedding runs in the UI and backend before training is queued. Require live Hub access for streaming dataset preflight instead of accepting unrelated cached data. Compare advanced settings against applied model defaults and cover the updated behavior with focused tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix training picker validation and platform safeguards Update the Tauri YAML contract test to verify save filter behavior without depending on the Rust vector implementation. Align frontend Hub resource validation and training provenance normalization with the shared repository ID rules, including valid underscore and maximum length identifiers. Reuse one frontend training method policy so Apple Silicon onboarding and the Train page consistently disable CPT and prevent unsupported onboarding completion. Add regression coverage for rejected Hub IDs and exact resume provenance. * Fix MLX training capability validation Detect hardware before platform validation so unsupported MLX configurations fail before a job is queued. Reject audio dataset training during server preflight while preserving the worker guard. Share Apple Silicon capability checks across model selection, onboarding, readiness, submission, and LoRA controls. Cover audio, CPT, embedding, LoftQ, DoRA, and warm-start detection with regression tests. * Fix training quit protection, translations, and preference reset Protect desktop quits while a training start is pending or a run is active, and keep the warning text accurate. Localize dataset upload details across every supported locale and require those keys in parity checks. Centralize the training picker and parameter mode storage keys so resetting local preferences clears current and legacy values. Add regression coverage for training activity transitions and preference reset ownership. * Preserve multimodal training support Keep upstream training eligibility authoritative while preserving separate vision, audio, and embedding capabilities. Classify dual audio and vision models for vision training without changing inference support or enabling pure audio training on MLX. Add coverage for model type constraints, backend type resolution, and MLX validation. Apply the pending Biome formatting fixes to the dataset selector and training wizard. * Fix training defaults retry and clean stale translations Allow model defaults retries after failed cache reconciliation without overwriting user-edited settings. Ignore stale fallback vision checks once a newer defaults request starts. Remove unused parameter description keys from every locale and cover the retry behavior in the training contract tests. * Document direct model identity imports Explain why the model identity helpers bypass the Hub barrel beside each lint suppression. * Preserve training settings across reloads Persist applied model defaults and their advanced settings baseline while refreshing transient model metadata without overwriting tuned hyperparameters. Keep remote format probe tests runnable without an installed huggingface_hub package by supplying a fake module. Normalize tilde-prefixed path separators while preserving case-sensitive identities. Remove unmatched model and dataset tour anchors. Add regression coverage for persistence migration and model identity normalization. * Harden train page validation and persistence Validate CPT embedding learning rates against backend bounds and add localized client feedback. Keep invalid learning rate drafts out of persisted state and YAML exports. Notify users when multimodal model selection restores a non-S3 dataset source. Remove W&B tokens from training config persistence and migrate stored secrets safely. Update the model defaults contract test to cover metadata refresh without requiring the removed early return. Move segmented control styles into a non-component module to preserve Fast Refresh behavior. Add validation and persistence coverage for the new contracts. * Fix training model IDs, cache state, and localization Preserve root-level Hugging Face model IDs instead of rewriting them into the Unsloth namespace. Debounce token-scoped inventory reconciliation so token and inventory updates use stable request keys. Keep the selected training history run when the Studio view remounts. Localize dataset subset and split selectors across every supported locale. Complete Italian picker and training translations and enforce Studio-wide locale parity. * Fix training history errors and stale stop state Return structured artifact deletion errors and show localized messages that distinguish active training output from filesystem failures while keeping history intact. Clear superseded stop requests only when they belong to the current runtime generation, and cover start invalidation and stale stop handling with regression tests. Clarify that shared picker styles apply outside the Hub while Hub-only rules remain scoped. * Fix training method persistence and settings summaries Persist training method provenance so manual learning rates, model adapter rates, and pre-CPT dataset formats survive reloads and method changes. Deduplicate imported target modules and compare advanced-setting arrays by value counts so duplicate entries cannot hide non-default settings. Add migration and regression coverage for legacy state, rehydrated method transitions, restored learning rates, and duplicate target modules. * Fix persisted completion defaults and inventory retries Persist trainOnCompletions across reloads so model defaults and advanced settings summaries remain accurate. Defer missing legacy values until model metadata loads, preserving tuned settings while respecting streaming, raw text, CPT, embedding models, and explicit user changes. Settle partial inventory failures when usable rows exist so manual local model paths and Enter submission remain available. Add retry actions for partially failed model and dataset scans, including when filtering leaves no visible results. Add regression coverage for persistence migration, constrained completion defaults, inventory settlement, and picker empty states. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix training validation and route state regressions Retry newly detected image datasets with vision-aware validation, clear stale dataset check failures, and cover the failed background classification case. Restore Train history cleanup and centralize the learning recipes navigation intent key. Repair backend route stubs for canonical model IDs and path normalization, and isolate consent tests from shared package import state. * Preserve training cache pins during preflight Keep model and dataset cache flags and local paths in the pending start comparison so reconciliation cannot submit a stale snapshot. Cover changed cached copies and uncached transitions with focused regression assertions. * Preserve Hub identity for cached training exports Keep exact cached snapshots for training while restoring the standard Hub repository identity before PEFT saves. Recover repository identity in memory for legacy adapters so imatrix exports work without rewriting adapter configs. Cover repository mismatches, local models, and Windows cache paths with focused regressions. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix cached training, evaluation, and MLX resume behavior Limit cached dataset slice loads so small training ranges do not materialize full duplicate caches. Restore Hub model identity for cached Torch and MLX runs so saved adapters remain portable. Attest MLX model provenance across runtime and prequantized 4-bit formats so valid checkpoints can resume. Recognize missing cached eval splits, reload train and eval together, treat explicit eval split errors as fatal, and use deterministic held-out data for automatic fallback. Retain non-fatal evaluation warnings in training status and render them in the train UI. Apply pin and model format validation to every MLX worker entry point and keep the worker test fixture aligned with its imports. * Stabilize training progress updates across job handoffs Keep active and pending training ownership separate so status polling cannot switch identities during a running job. Scope metrics, progress streams, stop, and reset operations to the expected job and recheck ownership during handoffs. Make polling, stream parsing, and start reconciliation abortable, monotonic, and safe against stale overlapping requests. Preserve same-run interface state so live updates do not remount progress controls or dismiss the stop confirmation. Add backend and frontend regression coverage for ownership changes, stale events, request races, and stream cleanup. * Fix cached training fallback and dataset reconciliation Recognize incomplete SentencePiece tokenizer caches and retry online model loads through the Hub while preserving strict offline and resume pins. Prevent rejected dataset snapshots from being promoted again until the selection or inventory changes. Forward cancellation signals to dataset format requests so superseded checks stop at the transport layer. Add focused backend and frontend regression coverage for these cache paths. * Harden offline training and cache fallback behavior Validate cached model snapshots for tokenizer and processor support, then fall back to the pinned Hub revision when the cache is incomplete. Remember rejected dataset cache entries and cancel superseded checks so metadata-only snapshots cannot trigger request loops. Reject deleted local datasets before any Hub access and clear only the matching stale selection in background, preview, and start flows. Reserve training starts before validation so overlapping requests and GPU consumers cannot race the spawn window. Keep GGUF fallback metadata tied to the cache that supplied variants when Hub access fails. Filter DSpark and DFlash drafter companions across picker, loading, inventory, and deletion paths while preserving MTP behavior. * Harden training setup and picker behavior Keep training starts blocked while a stop request is pending and preserve the stop latch through backend failures. Skip malformed training progress events while validating SSE payloads and releasing stream readers safely. Enforce non-streaming state for upload and S3 datasets across source changes, persistence, and rehydration. Route Markdown dataset drops to Data Recipes across browser and native path formats. Give model and dataset source tablists localized accessible names. Refresh training start contracts and add focused regression coverage for the corrected behavior. * Fix offline dataset cache selection and token status Prefer finalized processed dataset caches for offline training while keeping raw cache paths available for scoped management. Treat malformed Hugging Face tokens as unset in Train and surface local validation feedback without unnecessary requests. Add regression coverage for cache coexistence, interrupted cache builds, path propagation, and malformed token presentation. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Preserve sidecar Hub version during dataset cache checks Keep processed dataset cache discovery free of eager datasets imports so training does not preload the base Hugging Face Hub package. Preserve cache root discovery through HF_DATASETS_CACHE, HF_HOME, and XDG_CACHE_HOME before Transformers sidecar activation. * Add the AGPL-3.0 header to the two new studio training contract tests * Scan the fallback target and pin cached snapshots that hold weights Cache fallback scanned the pinned snapshot it was about to discard, then loaded the Hub repo unscanned. Scan after the pin is dropped instead, in all four training paths. Preflight probed the literal model name, so the registry bicodec alias Spark-TTS-0.5B/LLM 404'd and rejected a supported model. Probe the repo the trainer downloads and treat its load subdir as the weight root. An unpinned start still downloads its dataset, so a stray cached copy no longer skips Hub verification. Offline keeps accepting the cache. refs/main can point at a metadata-only revision, so the model pin now prefers a snapshot that carries weights before falling back. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Let the capability-cache guard see through the key type alias model_config.py names the cache key _CapabilityCacheKey, so matching the literal Dict[Tuple no longer finds it and the guard fails on a cache that is still correctly tuple-keyed. Resolve module-level aliases first. Checked by mutation: reverting a cache to Dict[str, ...] is still caught. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: point the extra-UI tour anchor check at studio-model-picker The train page rework renamed the model tour anchor from studio-model to studio-model-picker and updated tour/steps/base-model.tsx accordingly, but tests/studio/playwright_extra_ui.py still looked for the old anchor, so the Chat UI Tests job failed with "[data-tour='studio-model'] not found". Verified against two live Studio instances: the anchor is studio-model on main and studio-model-picker at this head; studio-dataset and studio-params are unchanged on both. * Studio: require metadata and weights together when pinning a model snapshot Pass 1 of _resolve_model_snapshot matched on weights alone, so two cases still selected a snapshot that /training/start then rejects with "does not contain trainable weights": - a newer weights-only fetch (interrupted download, or an allow_patterns pull that never took config.json) displaced an older complete sibling. That is a regression against the previous metadata-first ordering, which started the run; reproduced with a two-snapshot cache where the complete one is older. - consolidated.safetensors counted as weights for selection but is absent from _MODEL_WEIGHT_CANDIDATES in routes/training.py, and transformers 4.57.6 has no loader path for it (zero references in the package), so a config plus consolidated snapshot won over a loadable sibling. Pass 1 now demands metadata AND weights via a required_groups argument on latest_snapshot_from_cache_path, and consolidated.safetensors is dropped from the selection tuple so it matches what the route accepts. Pass 2 keeps the metadata-only fallback unchanged, so caches that never held weights resolve exactly as before. Both new tests in test_model_cache_snapshot.py fail without this change and pass with it; 401 tests across the cache, preflight, provenance and identity suites pass. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: make the new cache tests pass on Windows Four of the PR's new tests fail on windows-latest while passing on ubuntu and macos. All four are defects in the tests, not in the production code. The three blob-symlink tests hardcode a POSIX relative target ("../../blobs/x"). Windows stores the reparse-point substitute name verbatim and resolves it in the object namespace, where / is not a separator, so the link is created but dangles: Path.resolve(strict=True) raises and the provenance and dataset-cache helpers correctly return None. huggingface_hub builds these targets with os.path.relpath (file_download.py _create_symlink), which yields ..\..\blobs\x on Windows, so a real cache never has this shape and no Windows user is affected. Building the target with os.path.relpath / os.path.join matches what huggingface_hub writes. The cross-snapshot rejection tests had the same POSIX targets, so on Windows they were passing for the wrong reason (dangling link rather than the escape check). They now use native separators too, so the rejection logic is actually exercised there. test_runtime_4bit_resume_reaches_worker_with_source_resource_pins compared model_local_path against str(Path); the route posix-normalizes that field via normalize_path, so the assertion now uses Path.as_posix(), which is a no-op on POSIX. 324 tests across the three files pass on Linux. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: bound the cached dataset re-check and stop adopting unconfirmed starts Two defects introduced by this PR's new modules. 1. Unbounded dataset re-check (#7853). training-config-store.ts re-runs the cached format check whenever rejectValidation() reports the in-flight token as stale, with no counter and no backoff. DatasetCacheRejectionTracker advances its generation on any inventory-fingerprint change, and the fingerprint includes sizeBytes, so a dataset that is still downloading invalidates every check in flight and the pair never converges. Measured at 480 requests in 60s; a probe driving the real tracker ran 500 iterations without settling while a stable inventory settles in 1. The retry now draws from a per-selection budget (dataset-recheck-budget.ts) and falls through to the uncached check once spent, so the answer still refreshes when the inventory genuinely changes but a churning one cannot spin. The budget resets when the dataset or split changes. Fixing the tracker fingerprint instead was rejected: existing tests pin the behaviour that a sizeBytes change makes a rejected cache retryable. 2. Unconfirmed starts adopted as recovered. reconcileTrainingStartTransportFailure ended with adoptAcceptedTrainingStart(pending.jobId, ...) and returned "recovered". The backend reserves the request id and job id before the heavy preflight, so a start still pending when the 30s window closes may yet be rejected. Adopting there reported success and pinned a job id that never became current_job_id: acknowledgeTrainingStartRequest was never sent, and dismissTrainingRun's expectedJobId check then returned "superseded", leaving the rejected state unclearable until a new start re-reserved it. It now calls the module's existing settleUnconfirmedTrainingStart and returns "unknown", which both callers already handle by warning startUnconfirmed and settling unconfirmed, and which preserves start_request_id for acknowledgement. New tests fail without each change and pass with it; removing the bound makes them fail rather than hang. typecheck, 598 frontend tests, build, and the 154 studio contract tests all pass. * Studio: retry tokenizer-less pinned snapshots, and skip Hub preflight when offline Two more defects introduced by this rework. Neither mechanism exists on main: worker.py there has 0 occurrences of local_files_only / model_snapshot_path / cache_artifact, and routes/training.py has 0 of model_info / dataset_info. 1. Tokenizer-less pinned snapshot is terminal (#7845). The pin only requires config.json plus a weights file, so a snapshot with no tokenizer pins clean, local_files_only is set, and AutoTokenizer then fails. Recovery is gated on _is_model_cache_artifact_error, whose marker list matches only three of the message shapes transformers actually emits. Sweeping all 159 tokenizer classes against a tokenizer-less snapshot: 37 failures were classified not retryable, of which 26 are genuine cache problems that got zero Hub retry, including XLMRoberta (BGE-M3, multilingual-e5, LaBSE), MBart, NLLB, Bloom, GPTNeoX, Cohere, Marian and the generic PreTrainedTokenizerFast. SentencePiece and BPE families resolve a missing vocab path to None and then dereference it, so the failure arrives as a bare AttributeError with no cache-specific text. Adding those four shapes takes the sweep from 37 misses to 11, and all 11 remaining are correctly fatal (missing optional Python dependency, or an unsupported tokenizer class), which no Hub retry can fix. Widening the classifier rather than validating tokenizers before pinning: the vocab filename space is as open-ended as the exception space across 159 classes, requiring a tokenizer would break the deliberate adapter_config.json pin path, and a real loadability check means loading a tokenizer inside the start request. The recovery path is already gated on offline and require_exact, so a false positive costs one Hub attempt while the current false negative fails the run. 2. Blocking Hub preflight with no reachability guard. Both preflight legs retry metadata at 5s then 10s with no reachability check, and requests applies the timeout per resolved address. Measured 30.0s added to a single POST /training/start with the Hub black-holed, scaling with the number of addresses, surfacing as 503. utils.utils already provides a bounded, memoised hf_unreachable/hf_dns_dead that the training worker subprocess uses one layer down; the route that spawns it did not consult it. The model leg raises inside the existing try, so the except HTTPException handler still runs _resolve_model_snapshot and a cached snapshot pins exactly as before, just without first burning the remote budget. The guard fails open, so an online start is unchanged. New tests fail without each change and pass with it, including a wiring contract that fails if the guard stops being consulted. 443 tests across the preflight, cached-start, provenance, snapshot and streaming suites pass. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Make the import-hoist lint honour __all__ re-exports Source lint failed on studio/backend/utils/security/__init__.py: [BLOCKER] HOISTED-IMPORT-UNUSED 'load_scan_target' (['from:utils.security.file_security:load_scan_target']) added but unused That file is a barrel __init__: every name it imports carries "# noqa: F401" and is listed in __all__, and routes/training.py imports load_scan_target from the package rather than the submodule, so the re-export is load-bearing. HOISTED-IMPORT-UNUSED only excluded re-exports that already existed before the change, so adding any new name to an existing barrel was an automatic blocker. The script's own docstring already records this shape as a known false positive for NEW-UNUSED-IMPORT. Skip module-level imports whose bound name appears in __all__, since those are loaded by importers rather than by the module itself. The rule keeps its teeth: a newly added import whose name is not in __all__ still blocks, and a dangling alias still trips UNRESOLVED-NEW. Verified with a four-case mutation matrix plus --self-test, and the full 79-file changed-set lint now reports OVERALL: PASS. * Keep studio off evaluated PEP 604 unions on the 3.9 floor tests/test_python39_compatibility.py failed on Core (HF=4.57.6, default and latest) and Repo tests (CPU): test_no_pep604_unions_are_evaluated_on_the_declared_floor model_config.py:937: Tuple[...] | Tuple[...] (type alias) test_studio_evaluated_unions_do_not_grow 36 studio files now evaluate PEP 604 unions, up from 35 Both offenders came from this branch. model_config.py:937 is a module-level type alias, so it is evaluated at import time and "from __future__ import annotations" cannot defer it; it needs typing.Union, which is what the test message prescribes. hub/utils/dataset_cache.py is the one file that pushed the count to 36, and all five of its unions are function annotations, so the future import is enough there. No behaviour change: the alias is only a cache-key type, and the annotations are unevaluated either way. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Key the dataset re-check budget on the whole selection The budget added for #7853 keyed on dataset + split only, but a dataset cache usability identity has four user-chosen dimensions: dataset, subset, split and streaming. Changing subset or toggling streaming therefore kept the same key, so a genuinely different selection inherited an exhausted budget, skipped the cache-preferring re-check and dropped straight to a remote resolution with datasetKnownCached cleared. Key on all four instead, JSON-encoded so no delimiter can collide with a name containing it and null stays distinguishable from the string "null". cachePath is deliberately still excluded even though the usability identity carries it. It is derived state that moves as a download populates the cache, so feeding it into the key would mint a fresh budget on every poll and re-arm the exact non-terminating loop this module exists to bound. Tests: two new cases cover subset and streaming, and an inverse case pins that nothing outside the selection can refresh the budget. Reverting the key to dataset::split fails exactly the two new cases and no others. * Stop the pinned snapshot path reaching PEFT as the base model name A completed run wrote a machine-local path as the adapter's base model: base_model_name_or_path = '/home/user/.cache/huggingface/hub/ models--unsloth--Llama-3.2-1B-Instruct/snapshots/0123…' where main writes the Hub id. It lands in adapter_config.json, every checkpoint-*/adapter_config.json, the run card, export_metadata.json for merged and GGUF exports, and the model card push_to_hub uploads, none of which resolve on another machine. restore_hf_cache_repo_identity runs in UnslothTrainer.load_model before get_peft_model, so its peft_config branch has nothing to repair yet, and PEFT then derives the name itself: # peft/mapping_func.py new_name = model.__dict__.get("name_or_path", None) peft_config.base_model_name_or_path = new_name PreTrainedModel.__init__ copies config.name_or_path onto the instance, so restoring only config._name_or_path leaves that slot holding the snapshot path. Restore the instance attribute too. It goes through the same guard as the other fields, so an ordinary local model, an unrelated Hub id and a repo mismatch are all still left alone. Tests are behavioural: the existing model identity suite asserts the call site via AST and stays green with this bug present, which is why it was missed. Removing the new line fails 3 of the 6 added tests and none of the other 11. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolve cached snapshots that load from a subdirectory unsloth/Spark-TTS-0.5B keeps everything trainable under LLM/: its snapshot root carries only README.md and config.yaml, no config.json and no weights. _resolve_model_snapshot only looked at the root, so a cached copy resolved to None. _apply_model_cache_pin then warned "Cached copy not found on disk; downloading", and offline the start route turned the same None into a 409 hf_model_not_cached_offline for a model that was sitting in the cache. The remote preflight already handles this: it expands load roots through load_scan_target. Only the cached path disagreed. Reuse security_load_subdirs, which already reports ("LLM",) for BiCodec, so both paths share one source of truth, and apply it to the resume and preflight pin lookups too rather than just the fresh-start one. Detection can raise offline or for a gated repo, so a failure degrades to root-only rather than propagating. Tests build the real models--org--name/snapshots/<rev> layout. Making the helper a no-op fails 3 of the 6, and a snapshot with nothing loadable in either place still resolves to None, so the widening cannot mask an empty cache. * Use Optional in the deprecated dataset alias signatures The rewritten alias module annotates two parameters as UploadFile | None and str | None with no postponed annotations, so they evaluate at import on the declared 3.9 floor while the rest of the file already uses Optional[...]. The file was an evaluated-union offender before this change too, so this is not a new break, just keeping the new code consistent with its own convention and off the debt list. * Apply the repo kwarg-spacing formatter to the new code pre-commit.ci flagged the ruff-format-with-kwargs hook on this branch. Running scripts/run_ruff_format.py locally keeps the fix in the authoring commits instead of trailing a separate bot commit. * Say why a resume is refused instead of blaming the checkpoint can_resume_run gained a provenance clause in this rework, so it now returns False for runs whose checkpoint is entirely intact, most realistically once the pinned model snapshot is evicted from the HF cache. The start route answered every False with "Resume checkpoint must belong to a stopped or errored run with complete saved trainer state." pointing the user at trainer state that is fine, while the real cause was the resource gate. exact_resume_resource_requirements already raises with a precise explanation and resource_provenance_allows_resume was discarding it. Add resource_provenance_resume_blocker, which returns that explanation (or None when the run is resumable), define allows_resume in terms of it so the two cannot drift, and use it in the start route when it is the actual cause. The gate itself is unchanged: refusing an unattested resume is deliberate, only the diagnosis was wrong. Tests are behavioural, with one narrow wiring contract for the branch that regressed. Replacing the precise reason with a generic string fails the message test and nothing else. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Honour load subdirs everywhere a cached snapshot is probed Follow-up to 590ac9f22. Resolving cached Spark-TTS/BiCodec snapshots from their LLM/ load root got a start past _resolve_model_snapshot, but three later probes still asked for root-level config.json only, so the same snapshot was accepted in one place and rejected in the next: - provenance.py attested incomplete model provenance, after which exact_resume_resource_requirements refused resume for a snapshot still sitting on disk; - the start preflight reported a valid cached model as having no trainable weights; - the worker cleared model_snapshot_path before loading, falling back to the Hub instead of the selected snapshot. Promote the helper to hub.utils.hf_cache_state.with_load_subdirs so there is one definition rather than four, and use it at every site. _has_trainable_local_weights takes subdirectory roots instead, since it probes directories rather than a filename list. Unchanged for ordinary models: with no load subdirs the helper returns its input, and detection failures still degrade to root-only. * Let the model routes see the same cached snapshots training does Two ways /api/models disagreed with the training resolver, both reachable from resume: _model_config_inspection_target probed only the snapshot root, so a cached Spark-TTS/BiCodec copy answered "Selected cached model is no longer available" for a cache the training resolver accepts, and the exact-snapshot remote-code scan could fail with it. It now uses the shared with_load_subdirs helper. The model_snapshot_repo_id guard used the owner/repo-only regex, so resuming or scanning a namespace-less Hub model such as gpt2 or bert-base-uncased returned 400 before the snapshot could be inspected, even though hub.utils.paths .is_valid_repo_id and the picker both allow the one-segment form. That call site now uses the shared validator. The other five uses of the local regex predate this branch and are left alone. Reverting either fix fails one of the new tests and nothing else. * Get the resume refusal reason all the way to the user 300fe6321 fixed the diagnosis on POST /api/train/start, but the History UI never reaches it. can_resume: false hides the Resume button outright, and in the one window where the server could answer, resume-training-run.ts throws its own "Only stopped or errored runs with a saved checkpoint can be resumed" before sending any request, so no start call is ever made. For a run whose checkpoint is intact and whose pinned snapshot was evicted, that sentence is the wrong diagnosis, which is what 300fe6321 removed server-side. Carry the reason on TrainingRunSummary and prefer it in that guard. The field is optional and defaults to None, so old clients are unaffected, and it is only computed for rows already known unresumable. A checkpoint problem still reports None, leaving the client's existing wording for that case, and a failure inside the gate is swallowed so History still renders. Reverting either half fails one of the new tests: the summary stops carrying the reason, or the client stops preferring it. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Honour load subdirs on the resume pin and keep the selected model cache copy Two follow-ups to the cached-snapshot work. The resume branch of _reject_untrainable_model_request still probed the pinned snapshot with a hardcoded (config.json, adapter_config.json) tuple, so a repo that loads from a subdirectory (unsloth/Spark-TTS-0.5B keeps everything trainable under LLM/) resolved to None. Offline that became a 409 hf_model_not_cached_offline for a snapshot present on disk; online it fell through to the remote metadata round trip the pin exists to avoid. It now uses with_load_subdirs like the other cached-snapshot probes. The cache reconciliation effect took the first usable inventory row for the selected repo. A repo can be present under more than one HF cache root, so that silently retargeted an explicit selection at a different copy on the next inventory tick. The selection logic moved to a pure module and now prefers a row whose path matches the current selection, falling back to the first usable row when there is no selection or the selected copy has gone. Tests: test_resume_pin_load_subdirs.py (5) and model-cache-reference-selection.test.ts (8); reverting either fix fails them. * Revert the model cache reference preference; it cannot fire The frontend half of 2c2a95309 assumed the model inventory can return more than one usable row for a repo. It cannot. hub/services/models/cache_inventory.py::_scan_cached_models collects into seen_lower keyed by repo_id.lower() and resolves collisions with _prefer_cache_row, and _dedupe_local_models keys hf_cache rows by (model_id, model_format, format_variant). Both return one row per repo, and a live check with the same repo under four HF cache roots got exactly one row from /api/hub/cached-models and one from /api/hub/local. With a single usable row usable.find(pred) ?? usable[0] is identically usable[0], so the change was a no-op and its tests asserted an array shape the API cannot emit. The resume pin fix in the same commit stands: that one was reproduced against the real unsloth/Spark-TTS-0.5B cache and reverting its hunk restores the 409. * Report the resume refusal that actually happened 300fe6321 and a98e721f4 set out to stop a provenance refusal being reported as a checkpoint problem. They overshot. Both sites asked resource_provenance_resume_blocker whenever can_resume_run said no, but that function refuses for several reasons and the blocker is computed independently of which one fired. initialize_resource_provenance writes {version: 1, status: pending} at the start of every run, so the blocker answers "The model revision used by this run was not attested." for any Hub-model run, including one whose checkpoint is simply missing. That is the more common way to be unresumable, so the change traded one misdiagnosis for another and the new one covered the larger population. Both sites now gate on has_resume_state, the same discriminator can_resume_run short-circuits on: no saved trainer state means the checkpoint is the cause and the client's own wording is correct; an intact checkpoint means a refusal really is provenance's doing and the specific reason is worth surfacing. test_resume_reason_matches_cause.py pins it; reverting either guard fails it. The route contract assertion is over the AST rather than the source text, because a substring search is satisfied by the explanatory comment beside the code and passes with the guard deleted. Two assertions in test_resume_blocked_reason_surfaces.py encoded the old behaviour and are corrected, including the docstring claiming History survives a raising gate: can_resume_run calls the same gate unguarded one line earlier, so that only holds when it short-circuits first. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Scope the import-hoist __all__ skip to package __init__.py 68bce3934 skipped any name listed in __all__. The comment said "the whole point of a package __init__", but compare() never consulted the path, so the skip applied to every module defining __all__ -- 27 non-package modules exempting 224 names, unsloth/models/_utils.py alone 74. The cost is not just breadth. lint-ci.yml names rename-clash as one of the two bugs this tool exists to catch because ruff and pyflakes miss it, and the skip disabled that detection for any name in __all__. Adding a name there became a one-line, reviewer-invisible way to switch the check off. The self-test I cited did not cover this: --self-test reported ALL PASS with the skip hunk reverted, because every case ran through the "<name>" path placeholder and none defined __all__. Cases may now carry their own path, and three new ones pin the behaviour from both sides -- a re-export in a package __init__ is allowed, the same shape in an ordinary module is still blocked, and a new import absent from __all__ is still blocked in an __init__ too. Deleting the skip fails the first; widening it back fails the second. Removes two genuinely unused imports the correctly-scoped check then found in test_resume_blocked_reason_surfaces.py. * Make deleting a run's artifacts reversible until the row is gone DELETE /runs/{run_id}?delete_artifacts=true is new in this PR; onb41b819a4the endpoint only calls delete_run and never touches the filesystem. As written it rmtree'd the output directory and then deleted the row, so a failing row delete left the artifacts destroyed and the row alive with output_dir still populated. storage/studio_db.py opens SQLite with Python's default 5s busy timeout and sets no busy_timeout pragma, so a writer holding the database longer than that is enough. The state that leaves is the real problem: a row whose artifacts are silently gone is indistinguishable from the legitimate keep-history outcome the same endpoint produces for a shared output directory, so the user cannot tell which happened. A retry does recover, but only because a missing directory is treated as success, which is incidental rather than designed. _delete_run_output_dir now performs a same-parent rename to .<name>.deleting-<uuid> and returns the staged path. The run is logically gone the moment that succeeds, but the bytes survive until delete_run commits; a failure restores the rename and the operation rolls back whole. The active and shared guards still run first, inside the same lifecycle guard, so neither can be raced. Three assertions in the author's test_training_history_delete.py move to the new return shape; their subjects, the lifecycle guard and the shared-output recheck, are unchanged and still pass. * Keep cached-snapshot resolution off the network 590ac9f22 routed the cached-snapshot resolvers through security_load_subdirs, which calls detect_audio_type. That function only skips its remote tokenizer fetch when local_files_only is set, and the new callers did not set it, so _resolve_model_snapshot and the two cache-pin sites -- all pure filesystem work before -- gained a hub round trip with no timeout in front of them. On a slow or hung hub, asking whether a snapshot is already on disk could stall. security_load_subdirs gains an opt-in local_files_only, default unchanged so the security scanner keeps the remote answer it wants. with_load_subdirs passes it: the subdir layout is a property of the snapshot on disk, so the local answer is also the correct one there. A side effect worth noting: a network failure previously raised straight past the YAML registry fallback, because security_load_subdirs wraps both branches in one try. Asked offline, detection reports nothing instead of raising, so the fallback now gets its turn. That comment-versus-code mismatch is byte-identical onb41b819a4and is left alone, but it is pinned by a test so it stays a decision rather than a surprise. Three existing fixtures stubbed security_load_subdirs with a two-argument lambda and silently lost the expansion through the helper's except; they now carry the new keyword. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Pin the two load-subdir sites nothing was guarding An audit ran the full backend suite against ca7c72e75's three sites reverted one at a time. Reverting core/training/provenance.py or core/training/worker.py left 17204 passing tests green with a byte-identical failure set: both shipped undetectable. Only routes/training.py was caught, and only retroactively, by a file added in a later commit. Neither is decorative. For a subdirectory-loading repo the provenance site turns a snapshot present on disk into "The exact model snapshot for this run is no longer available." and refuses the resume; the worker site either errors with "The cached model snapshot selected during preflight is no longer available." under strict resume or, without it, silently drops the pin so the load goes back to the Hub. test_subdir_pins_are_guarded.py covers both, at the helper and at the user-visible gate. Reverting the provenance expansion fails 3 of the 6; reverting the worker expansion fails the non-strict pin test. Empty and wrong-subdir snapshots are still rejected, so the widening cannot mask an unusable cache. Also adds the debug line the shared except never had. Degrading to root-only is fail-closed everywhere, but a genuine cache permission or corruption fault reaches the user as "your cached model isn't cached" with no diagnostic, and four sites now share that handler. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bind has_resume_state in the alternative-layout import fallback routes/training.py imports its helpers in a try and repeats the block under except ImportError for an alternative on-disk layout. The resume diagnosis fix added has_resume_state to the primary list only, so wherever the fallback runs the name is undefined and a resume request with intact checkpoint state and a failed provenance check raises NameError -- a 500 in place of the refusal reason that change existed to produce. test_route_import_fallbacks_agree.py compares the two blocks across every module under routes/, so the class is covered rather than this one instance: any name imported from the same module in the primary branch has to appear in the fallback too. Removing has_resume_state from the fallback alone fails it. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Let provenance read metadata off an MLX model _object_value tried the mapping protocol before attribute access. mlx.nn.Module subclasses dict (MRO: Module -> dict -> object), so every probe on a real MLX model took the dict branch and answered None, and both MLX paths added for attestation were unreachable: getattr(model, "_unsloth_quantized_source") -> 'runtime' _object_value(model, "_unsloth_quantized_source") -> None _loaded_model_is_4bit(model) -> False _loaded_model_refs(model) -> set() unsloth_zoo does record the metadata (mlx/loader.py sets _unsloth_quantized_source at 2748 and _hf_repo / _unsloth_base_commit_hash at 4047-4050); provenance just could not see it. Net effect on Apple Silicon: a runtime-quantized run -- the Studio default of a 16-bit repo with the 4-bit toggle on -- never attested, with or without a cache pin, so its checkpoints were never resumable. Read the attribute first and keep the mapping lookup as the fallback, which the plain dicts flowing through here (quantization_config, _unsloth_quantization_policy) still need. Verified with real MLX LoRA runs, provenance status before -> after: SmolLM-135M-4bit unpinned incomplete -> attested / prequantized_4bit Qwen3-0.6B 4bit pinned incomplete -> attested / runtime_4bit Qwen3-0.6B 4bit unpinned incomplete -> attested / runtime_4bit Qwen3-0.6B 16bit unpinned incomplete -> attested / unquantized resource_provenance_allows_resume returns True for all four afterwards, and _loaded_model_refs now resolves the repo/commit pair it previously missed. The mlx doubles in test_training_provenance.py are SimpleNamespace, which is not a dict subclass, so they exercised a branch no MLX model reaches. The new test parametrizes over a plain object, a dict subclass, and a real mlx.nn.Module; the dict-subclass case reproduces the bug without MLX installed, so it guards on CI too, and the MLX case importorskips off Apple Silicon. Prequantized snapshots that are not uniformly 4-bit (an 8-bit mlx-community conversion, mixed per-layer widths) still do not attest. That is a separate cause in _snapshot_declares_quantization and is left alone here. (cherry picked from commit 8af94dc1fd225905aa031d08b59ad752b20112f0) * Tighten comments across the train-page rework Collapse multi-line comment blocks to their essential reasoning, drop comments that restate self-explanatory code, and shorten section banners. Comments recording a non-obvious why (reproduced bugs, upstream quirks, deliberate tradeoffs) are kept, just stated in fewer lines. Comments and docstrings only: verified with an AST comparison against the previous tree, so no code changed. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten comments across the remaining studio files Collapse multi-line explanations to a single line and drop a redundant section marker. Comments only, no code changes. * Fix offline dataset selection and training review regressions Resolve cached dataset configs and splits from trusted local metadata with a localized validated manual fallback. Require an explicit split for cached offline training while preserving remote and streaming format checks. Refresh the shared dataset inventory after full recipe completion so new artifacts appear on Train. Keep shared output deletion atomic and restore legacy dataset format column ordering. Keep dataset completion attestations on v2 while writing ordinary manifests in downgrade-compatible v1 and migrating prior ordinary v2 records at startup. Return stable codes for rejected training model selections. * Adapt the grad-norm payload test to this branch's config shape #7917 added tests/training-start-payload-grad-norm.test.ts against main's TrainingConfigState. This branch moved hfToken out of that state into a second parameter on buildTrainingStartPayload, dropped datasetUserTemplate and datasetAssistantTemplate, and added 11 required fields, so the merged file did not typecheck. Staging CI caught it; the local run had not covered the second merge yet. The literal now spreads initialTrainingConfigState and keeps only the fields the test actually varies, so a future field addition does not break it again. Still non-vacuous: reintroducing max_grad_norm: 0.0 in the mapper fails it with "max_grad_norm must be absent, not null and not 0", which is the regression the file exists to catch. typecheck clean, 634 frontend tests pass. * Scope pending training cancellation to its start request Keep the request identity until cancellation or exact status reconciliation completes. Reject cancelled starts before worker spawn and stop or reset only the job owned by that request. Fence the process-start race, clean up adoption failures, and keep unrelated jobs untouched. Remove explanatory comments added with the earlier dataset and manifest fixes. * [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 * Update the training-start contracts to the scoped cancellation shape Four assertions in tests/studio/test_hf_token_validation_tick_contract.py still described the pre-scoping implementation, so the Studio contract suite was red at 8443285df. isTrainingStartPending gained a startRequestId disjunct. Asserted per term rather than as one exact string, so the expression's formatting is not the contract. stopTrainingRun and dismissTrainingRun replaced the expectedJobId locals with trainingStopScope / runtimeMatchesStopScope, and the request now sits inside a try, so the transport slice cut at the scope === null early return instead of the call. Re-sliced on the try/catch and renamed the reads. The blanket "setStopRequested(false) not in stop" no longer holds: the start branch clears the latch on purpose so a pending-start cancel stays retryable. Split per branch instead, which is a stronger contract than before -- the job branch must still keep the latch, and the start branch must still clear it. Non-vacuous, checked both ways: dropping the startRequestId disjunct fails the pending test, and adding setStopRequested(false) to the job failure branch fails the cancel-lease test. 154 tests across the four contract files pass; tests/studio 2561 passed. * Offer the cached dataset options the start request will accept local_options.py starts the subset and split options the picker shows, but it was written with a different grammar from TrainingStartRequest, so it disagreed in both directions. _SPLIT_RE was \w+(?:\.\w+)* and dropped the hyphen, so a real split name such as train-clean (LibriSpeech) never reached the picker even though the start request accepts it. An offline user had to type it by hand. \w is Unicode-aware in Python, so trein with an accent was offered and then rejected by the ASCII-only split validator. _CONFIG_RE excluded only filesystem-hostile characters, so a config name containing a space was offered and then rejected by the subset validator. Both turn a click on an offered option into a 422. Both patterns now mirror _check_subset and _check_split_name, which are the authority, and _valid_option rejects ".." anywhere rather than only as a whole segment, matching the split validator. New test drives the real pydantic validators rather than restating their regexes, and asserts over the normalized value, since _valid_option strips and it is the stripped string that gets offered. Reverting either pattern fails 6 of its 14. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Report a failed artifact purge instead of claiming the delete succeeded Deleting a run with its artifacts stages the output directory with a same-parent rename and purges it once the database row is gone. The purge swallowed OSError while the response still said artifacts_deleted=true, so a failed rmtree stranded every byte under the hidden randomized .{name}.deleting-<hex> name: the row is gone by then, so nothing points at it and no retry can rediscover it. The purge now reports whether the bytes are actually gone, and puts the directory back under its own name when they are not. The response says the artifacts were kept, with a purge_failed reason, and the history grid's existing artifacts_deleted check surfaces it. Same window, other half: when the row delete and the restoring rename both fail, the response now names the staged path instead of raising a bare 500 that leaves the artifacts unreachable. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep offering cached subsets whose only oddity is a dotted name Aligning the picker's grammar with TrainingStartRequest went one step too far: _valid_option started refusing ".." anywhere, for both fields. _check_split_name does reject ".." outright, so that is right for splits, but _check_subset only constrains the charset, which means a config directory named v1..v2 is perfectly startable and the picker was hiding it. That is the same bug as offering an option the start request rejects, just pointing the other way. The ".." refusal is now scoped to split names. Neither charset admits a separator, so the bare "." and ".." names remain the only traversal shapes left to refuse for either field. test_processed_cache_options_are_local_deduplicated_and_non_train_capable pinned the pre-change behaviour for both names in its fixture. Its "config with spaces" entries stay dropped, since _check_subset rejects the space and offering it turns a click into a 422, and "v1..v2" comes back. * [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> Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: shimmyshimmer <michaelhan2050@gmail.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com> Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local> Co-authored-by: danielhanchen <unslothai@gmail.com> Co-authored-by: Bardia Koopah <bkoop2003@gmail.com>
292 lines
10 KiB
Python
292 lines
10 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Training progress callbacks must report an active status once training starts.
|
|
|
|
Both training paths used to leave the parent on the pre-train "Starting ..." status
|
|
for the whole run, so /api/train/status and the progress card read "Starting
|
|
training..." while the loss was already moving: the callbacks report an empty status
|
|
on every log and the parent only overwrites a non-empty one. These tests drive the
|
|
real callbacks, worker emit rule and parent handler. Fakes only; no GPU, no model.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import queue as _queue
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
|
|
if _BACKEND_DIR not in sys.path:
|
|
sys.path.insert(0, _BACKEND_DIR)
|
|
|
|
# core/training/trainer.py imports unsloth and trl at module level (heavy, GPU init). Stub
|
|
# whichever are missing just long enough to import it, then restore.
|
|
_STUBS = {
|
|
"unsloth": ("FastLanguageModel", "FastVisionModel", "is_bfloat16_supported"),
|
|
"unsloth.chat_templates": ("get_chat_template",),
|
|
"trl": ("SFTTrainer", "SFTConfig"),
|
|
}
|
|
_STUBBED: list[str] = []
|
|
_TRAINER_PRE_IMPORTED = "core.training.trainer" in sys.modules
|
|
|
|
|
|
def _stub_if_missing(name, attrs):
|
|
"""Stub ``name`` unless the real package is installed."""
|
|
if name in sys.modules:
|
|
return
|
|
try:
|
|
importlib.import_module(name)
|
|
return
|
|
except Exception:
|
|
pass
|
|
_STUBBED.append(name)
|
|
module = types.ModuleType(name)
|
|
# A spec-less module reads as "no namespace shadow" to ensure_real_packages.
|
|
module.__spec__ = None
|
|
for attr in attrs:
|
|
setattr(module, attr, MagicMock())
|
|
sys.modules[name] = module
|
|
parent, _, child = name.rpartition(".")
|
|
if parent and parent in sys.modules:
|
|
setattr(sys.modules[parent], child, module)
|
|
|
|
|
|
if not _TRAINER_PRE_IMPORTED:
|
|
for _name, _attrs in _STUBS.items():
|
|
_stub_if_missing(_name, _attrs)
|
|
|
|
from core.training.trainer import UnslothTrainer # noqa: E402
|
|
from core.training.training import TrainingBackend, _MLXTrainerAdapter # noqa: E402
|
|
from core.training.worker import ( # noqa: E402
|
|
_create_embedding_progress_callback,
|
|
_create_trainer_progress_callback,
|
|
)
|
|
|
|
if not _TRAINER_PRE_IMPORTED:
|
|
for _name in _STUBBED:
|
|
sys.modules.pop(_name, None)
|
|
# Drop the stub-bound module and its parent package so a later test re-imports it against the
|
|
# real packages; the UnslothTrainer class held above stays usable.
|
|
sys.modules.pop("core.training.trainer", None)
|
|
sys.modules.pop("core.training", None)
|
|
|
|
ACTIVE = "Training in progress..."
|
|
|
|
|
|
class _FakeQueue:
|
|
"""Stands in for the mp.Queue the worker sends events on."""
|
|
|
|
def __init__(self):
|
|
self.events: list[dict] = []
|
|
|
|
def put(self, event, *args, **kwargs):
|
|
self.events.append(event)
|
|
|
|
|
|
def _state():
|
|
return SimpleNamespace(global_step = 0, epoch = 0.0, num_input_tokens_seen = 0)
|
|
|
|
|
|
def _drive(
|
|
callback,
|
|
steps = 3,
|
|
control = None,
|
|
on_step = None,
|
|
):
|
|
"""Run the HuggingFace callback lifecycle the way Trainer.train() does."""
|
|
state = _state()
|
|
control = control if control is not None else SimpleNamespace(should_training_stop = False)
|
|
callback.on_train_begin(None, state, control)
|
|
for step in range(1, steps + 1):
|
|
state.global_step = step
|
|
state.epoch = round(0.5 * step, 2)
|
|
state.num_input_tokens_seen = 128 * step
|
|
callback.on_log(None, state, control, logs = {"loss": 1.0 / step, "learning_rate": 1e-4})
|
|
callback.on_step_end(None, state, control)
|
|
if on_step is not None:
|
|
on_step(step)
|
|
# Once at the end: HuggingFace calls on_epoch_end per epoch, not per step.
|
|
callback.on_epoch_end(None, state, control)
|
|
return state, control
|
|
|
|
|
|
# --- LLM/VLM/audio path: UnslothTrainer._create_progress_callback ->
|
|
# worker._create_trainer_progress_callback ---
|
|
|
|
|
|
def _make_owner():
|
|
# __new__ dispatches to the MLX adapter on Apple hardware, which has no
|
|
# _create_progress_callback; go straight to the class under test.
|
|
owner = object.__new__(UnslothTrainer)
|
|
UnslothTrainer.__init__(owner)
|
|
owner._update_progress(is_training = True, total_steps = 4, status_message = "Starting training...")
|
|
return owner
|
|
|
|
|
|
def test_train_begin_reports_active_status():
|
|
owner = _make_owner()
|
|
callback = owner._create_progress_callback()
|
|
|
|
callback.on_train_begin(None, _state(), SimpleNamespace())
|
|
|
|
assert owner.training_progress.status_message == ACTIVE
|
|
|
|
|
|
def test_logging_reports_an_empty_status_so_the_active_one_is_sent_once():
|
|
# The parent keeps the last non-empty status, so a run costs one status event.
|
|
owner = _make_owner()
|
|
reported: list[str] = []
|
|
owner.add_progress_callback(lambda progress: reported.append(progress.status_message))
|
|
|
|
_drive(owner._create_progress_callback(), steps = 3)
|
|
|
|
assert owner.training_progress.status_message == ""
|
|
assert [status for status in reported if status] == [ACTIVE]
|
|
assert owner.training_progress.step == 3
|
|
assert owner.training_progress.loss == pytest.approx(1 / 3)
|
|
assert owner.training_progress.num_tokens == 384
|
|
|
|
|
|
def test_parent_status_advances_over_the_whole_chain():
|
|
owner = _make_owner()
|
|
backend = TrainingBackend()
|
|
event_queue = _FakeQueue()
|
|
owner.add_progress_callback(_create_trainer_progress_callback(event_queue))
|
|
# The worker sends this right before trainer.train().
|
|
event_queue.put({"type": "status", "message": "Starting training...", "ts": 0.0})
|
|
|
|
_drive(owner._create_progress_callback(), steps = 3)
|
|
for event in event_queue.events:
|
|
backend._handle_event(event)
|
|
|
|
assert backend._progress.status_message == ACTIVE
|
|
assert backend._progress.step == 3
|
|
assert backend._progress.is_training is True
|
|
|
|
|
|
def test_training_warning_is_emitted_once_and_survives_later_status_updates():
|
|
owner = _make_owner()
|
|
backend = TrainingBackend()
|
|
event_queue = _FakeQueue()
|
|
owner.add_progress_callback(_create_trainer_progress_callback(event_queue))
|
|
|
|
owner._record_warning("Evaluation fell back to a held-out training split.")
|
|
owner._record_warning("Evaluation fell back to a held-out training split.")
|
|
owner._update_progress(status_message = ACTIVE)
|
|
for event in event_queue.events:
|
|
backend._handle_event(event)
|
|
|
|
warning_events = [event for event in event_queue.events if event["type"] == "warning"]
|
|
assert [event["message"] for event in warning_events] == [
|
|
"Evaluation fell back to a held-out training split."
|
|
]
|
|
assert backend._progress.warnings == ["Evaluation fell back to a held-out training split."]
|
|
assert backend._progress.status_message == ACTIVE
|
|
|
|
|
|
def test_mlx_adapter_deduplicates_warning_events():
|
|
adapter = _MLXTrainerAdapter()
|
|
|
|
adapter._handle_event({"type": "warning", "message": "Evaluation was disabled."})
|
|
adapter._handle_event({"type": "warning", "message": "Evaluation was disabled."})
|
|
adapter._handle_event({"type": "warning", "message": " "})
|
|
|
|
assert adapter.training_progress.warnings == ["Evaluation was disabled."]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"stop_status",
|
|
[
|
|
"Stopping training and saving checkpoint...",
|
|
"Cancelling training...",
|
|
],
|
|
)
|
|
def test_stop_status_is_never_replaced_by_the_active_one(stop_status):
|
|
owner = _make_owner()
|
|
backend = TrainingBackend()
|
|
event_queue = _FakeQueue()
|
|
owner.add_progress_callback(_create_trainer_progress_callback(event_queue))
|
|
callback = owner._create_progress_callback()
|
|
|
|
def _stop_after_first_step(step):
|
|
if step == 1:
|
|
owner.should_stop = True
|
|
owner._update_progress(status_message = stop_status)
|
|
|
|
_, control = _drive(callback, steps = 2, on_step = _stop_after_first_step)
|
|
# A resumed run re-enters on_train_begin; an already requested stop must survive.
|
|
callback.on_train_begin(None, _state(), SimpleNamespace())
|
|
for event in event_queue.events:
|
|
backend._handle_event(event)
|
|
|
|
assert [e["message"] for e in event_queue.events if e["type"] == "status"] == [
|
|
ACTIVE,
|
|
stop_status,
|
|
]
|
|
assert backend._progress.status_message == stop_status
|
|
assert control.should_training_stop is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Embedding path: worker._create_embedding_progress_callback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_embedding_callback(event_queue, should_stop = lambda: False):
|
|
return _create_embedding_progress_callback(
|
|
event_queue,
|
|
total_steps = 4,
|
|
training_start_time = 0.0,
|
|
should_stop = should_stop,
|
|
)
|
|
|
|
|
|
def test_embedding_parent_status_advances_over_the_whole_chain():
|
|
event_queue = _FakeQueue()
|
|
backend = TrainingBackend()
|
|
# The worker sends this right before trainer.train().
|
|
event_queue.put({"type": "status", "message": "Starting embedding training...", "ts": 0.0})
|
|
|
|
_drive(_make_embedding_callback(event_queue), steps = 3)
|
|
for event in event_queue.events:
|
|
backend._handle_event(event)
|
|
|
|
assert [e["message"] for e in event_queue.events if e["type"] == "status"] == [
|
|
"Starting embedding training...",
|
|
ACTIVE,
|
|
]
|
|
assert backend._progress.status_message == ACTIVE
|
|
assert backend._progress.step == 3
|
|
assert backend._progress.loss == pytest.approx(1 / 3)
|
|
assert backend._progress.total_steps == 4
|
|
|
|
|
|
def test_embedding_train_begin_reports_nothing_once_a_stop_was_requested():
|
|
event_queue = _FakeQueue()
|
|
control = SimpleNamespace(should_training_stop = False)
|
|
|
|
_drive(
|
|
_make_embedding_callback(event_queue, should_stop = lambda: True), steps = 1, control = control
|
|
)
|
|
|
|
assert [e for e in event_queue.events if e["type"] == "status"] == []
|
|
assert control.should_training_stop is True
|
|
|
|
|
|
def test_embedding_callback_survives_a_real_queue():
|
|
# The worker's queue is an mp.Queue; nothing put on it may be unpicklable.
|
|
import pickle
|
|
|
|
event_queue = _queue.Queue()
|
|
_drive(_make_embedding_callback(event_queue), steps = 1)
|
|
|
|
events = [event_queue.get_nowait() for _ in range(event_queue.qsize())]
|
|
assert [e["type"] for e in events] == ["status", "progress"]
|
|
assert pickle.loads(pickle.dumps(events)) == events
|