Skip to content

Studio: offer the latest transformers release for brand-new architectures - #7056

Merged
danielhanchen merged 75 commits into
mainfrom
studio-dynamic-latest-transformers
Jul 15, 2026
Merged

danielhanchen merged 75 commits into
mainfrom
studio-dynamic-latest-transformers

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Jul 10, 2026 •

Copy link
Copy Markdown
Member

Summary

Stacked on #7043. When a model's model_type is absent from every installed transformers overlay (a brand-new architecture), Studio today just fails to load it. This adds the escalation path: check whether the latest transformers release on PyPI supports the architecture, and if so let the user consent to installing that exact release into a new persistent sidecar, after which routing picks it up automatically on every future launch.

Right now this covers a real gap: the latest PyPI release (5.13.0) ships 26 model_types that none of our sidecars have, including cosmos3_omni, deepseek_v32, hunyuan_vl, and kimi_k25.

How it works

  1. utils/transformers_latest.py (new): latest_transformers_supports(model_type) fetches the latest release version from pypi.org/pypi/transformers/json and the auto-mapping sources for that release tag and for main from raw.githubusercontent.com, all unauthenticated (no GitHub login needed, api.github.com is never touched), parsed with the same no-exec AST extractor as the static router. Results are cached (memory + a JSON snapshot under studio root, 1 day TTL, atomic writes, failure backoff), fetches are bounded to 5s with one retry, and offline mode or UNSLOTH_STUDIO_NO_LATEST_TRANSFORMERS=1 disables the whole path.
  2. check_upgrade_for_model(model_name) fires only when the model_type is absent from the hardcoded tier tables AND every installed overlay, so no existing routing decision can ever change. It never raises; any failure means the current fallthrough behavior.
  3. POST /inference/validate now returns requires_transformers_upgrade plus {model_type, pypi_version, supported_in_pypi, supported_in_main}, following the existing requires_trust_remote_code consent pattern. A new POST /inference/install-latest-transformers endpoint re-verifies the version server-side and provisions the sidecar off-loop.
  4. Provisioning creates .venv_t5_latest exactly like the fixed sidecars (pip --target --no-deps, about 70 MB) with a JSON pin marker recording the exact version, so the venv persists across restarts and is picked up by routing (_TIER_RANK gains latest at the highest rank) with no further prompts.
  5. Compatibility gate: compat_plan(version) evaluates the release's requires_dist against the running environment with packaging, shadow-installs unsatisfied tokenizers/safetensors pins into the sidecar (same precedent as the llmcompressor shadow), and refuses the install with an explicit message for anything it cannot satisfy (a future numpy or torch floor) instead of failing later at model load. torch/peft/trl are never touched.
  6. If the architecture is only on transformers main (dev), the response says so and no install is offered (PyPI releases only for now).

Guarantees

  • Existing routing is byte-identical: the checker runs only after the hardcoded tables and all installed overlays miss. Parity verified over all 673 installed model_types.
  • Offline, kill-switch, and network-failure paths all return the exact current behavior.
  • The latest tier participates in probing and activation only once its venv exists and carries a valid pin.
  • Windows/macOS safe: pathlib/os.path only, os.replace atomic writes, sys.executable -m pip with the hidden-window kwargs, os.pathsep for PYTHONPATH.

Validation

  • 64 new tests (tests/test_transformers_latest.py) with the network mocked: signal shapes, cache/TTL/corrupt-cache/restart reuse, zero-fetch assertions for offline/kill-switch/known types, routing parity, provisioning (pin, injection rejection, no-pin-no-install), activation, probe-order gating, compat_plan.
  • Existing tests/test_transformers_version.py: 200 passed unchanged.
  • Live unauthenticated run from a dev box: PyPI latest 5.13.0 in 0.03s; release tag and main mappings parsed (674 and 678 model_types); cosmos3_omni correctly signaled; a real install produced a working 68 MB sidecar that a fresh process reused from its pin and routed to statically.

Frontend

The consent dialog ships in this PR. When /validate returns requires_transformers_upgrade, every explicit load path (chat runtime and the compare composer) pauses on a TransformersUpgradeDialog modeled on the remote-code consent dialog: it names the model_type and the latest PyPI version, and on Accept calls /inference/install-latest-transformers itself, shows an installing state with the buttons disabled, and resumes the original load automatically on success. Install failures surface in the dialog with a retry; Cancel aborts the load exactly like the trust dialog's deny path. Background auto-load skips upgrade-requiring candidates instead of prompting, mirroring the trust_remote_code rule, and the dialog runs before the security dialogs since no load can proceed without the runtime.

When no installable release exists, the dialog says so explicitly: an architecture only on transformers main gets the dev-only notice (Studio never installs development builds), and if the model also declares custom (auto_map) code it offers "Continue with custom code", resolving the paused load into the existing trust_remote_code consent gate as the last resort. Models with no custom code keep the Cancel-only notice, and architectures unknown to both PyPI and main produce no upgrade signal at all, so those still route straight to the unchanged security gate.

16-bit guard for sidecar loads

Live validation surfaced a generation crash when a latest-sidecar model kept the default bnb 4-bit quantization: transformers' grouped-MoE kernels feed packed uint8 expert weights into torch._grouped_mm and generation fails (16-bit works). A new latest_tier_active_for() mirrors the sidecar activation's tier resolution; the inference worker and the load route both flip load_in_4bit off for exactly those models, so the VRAM guard and the worker agree. Fixed tiers are untouched.

Live validation

Driven end to end with Playwright against a live Studio serving the built frontend, using Zyphra/ZAYA1-8B (model_type zaya, shipped by transformers 5.13.1, unknown to every installed tier, no custom code): pick the downloaded model in the chat picker -> "New model architecture" dialog naming zaya and 5.13.1 -> Install -> installing state -> sidecar provisioned and pinned -> dialog closes and the load continues automatically with latest tier routing -> model loads on GPU in 16-bit (35s) and answers prompts in chat (with its reasoning block). Re-validate afterwards reports requires_transformers_upgrade: false. The same flow was verified with a local fixture for a fake cosmos3_omni model. Frontend typecheck and production build pass; backend suites: 284 passed.

Follow-up (separate PR)

The same pre-flight for export/training starts (those paths do not call /inference/validate today).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for dynamically checking and installing the latest transformers release from PyPI or GitHub main to support brand-new model architectures. It adds new Pydantic models, API endpoints, a caching mechanism for remote mappings, dependency compatibility checks, and routing logic for a new user-consented .venv_t5_latest sidecar tier. The review feedback focuses on critical concurrency and performance improvements, such as releasing the global lock during blocking network fetches, serializing concurrent installation requests to prevent race conditions, caching set-converted model types to avoid repeated O(N) overhead, and safely handling exceptions during dependency marker evaluation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +211 to +234
def _get_snapshot() -> dict | None:
"""Current support snapshot: memory -> disk -> network, with TTL and failure backoff."""
global _memory_snapshot, _last_failure_at
with _lock:
if _snapshot_is_fresh(_memory_snapshot):
return _memory_snapshot
disk = _load_snapshot_file()
if _snapshot_is_fresh(disk):
_memory_snapshot = disk
return disk
if _disabled() or _env_offline():
return None
if time.time() - _last_failure_at < _FAILURE_BACKOFF_SECONDS:
return None
fresh = _refresh_snapshot()
if fresh is None:
_last_failure_at = time.time()
# A stale snapshot beats no answer for a "can this ever load" hint, but a
# stale positive could offer a version PyPI no longer serves; be strict and
# return None (graceful fallthrough to current behavior).
return None
_memory_snapshot = fresh
_save_snapshot_file(fresh)
return fresh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Holding the global _lock during blocking network requests in _refresh_snapshot() can block other threads in the ASGI thread pool, leading to latency spikes or thread starvation. Refactor _get_snapshot() to release the lock during the network fetch, using _is_fetching to prevent concurrent duplicate fetches.

def _get_snapshot() -> dict | None:
    """Current support snapshot: memory -> disk -> network, with TTL and failure backoff."""
    global _memory_snapshot, _last_failure_at, _is_fetching
    with _lock:
        if _snapshot_is_fresh(_memory_snapshot):
            return _memory_snapshot
        disk = _load_snapshot_file()
        if _snapshot_is_fresh(disk):
            _memory_snapshot = disk
            return disk
        if _disabled() or _env_offline():
            return None
        if _is_fetching:
            return disk
        if time.time() - _last_failure_at < _FAILURE_BACKOFF_SECONDS:
            return disk
        _is_fetching = True

    try:
        fresh = _refresh_snapshot()
    finally:
        with _lock:
            _is_fetching = False

    with _lock:
        if fresh is None:
            _last_failure_at = time.time()
            return disk
        _memory_snapshot = fresh
        _save_snapshot_file(fresh)
        return fresh

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3c36aa8: the network refresh now runs outside the lock with an in-flight flag deduplicating concurrent refreshes; losers return None (the graceful fallthrough) instead of blocking.

Comment on lines +450 to +451
def install_latest_transformers(version: str) -> dict:
"""Consented install of the latest transformers sidecar; returns a structured result.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Concurrent calls to install_latest_transformers can lead to race conditions where multiple threads attempt to delete and recreate the .venv_t5_latest directory simultaneously. To prevent this safely without blocking other critical operations during the long-running package installation, use an _is_installing flag guarded by _install_lock to serialize requests, while executing the actual installation outside of the locked section.

def install_latest_transformers(version: str) -> dict:
    """Consented install of the latest transformers sidecar; returns a structured result.

    Guards: the requested *version* must match the current PyPI latest from the (cached)
    snapshot, so a client cannot pin an arbitrary package version through this endpoint.
    On success ``.venv_t5_latest`` is provisioned and pinned; routing then resolves the
    new tier automatically on this and every future start.
    """
    global _is_installing
    with _install_lock:
        if _is_installing:
            return {
                "success": False,
                "version": version,
                "message": "An installation is already in progress.",
            }
        if _disabled():
            return {
                "success": False,
                "version": version,
                "message": "Latest-transformers installs are disabled "
                "(UNSLOTH_STUDIO_NO_LATEST_TRANSFORMERS).",
            }
        if _env_offline():
            return {
                "success": False,
                "version": version,
                "message": "Cannot install: Studio is in offline mode.",
            }
        snapshot = _get_snapshot()
        if snapshot is None:
            return {
                "success": False,
                "version": version,
                "message": "Could not verify the latest transformers release on PyPI.",
            }
        if version != snapshot["pypi_version"]:
            return {
                "success": False,
                "version": version,
                "message": f"Requested version {version!r} is not the latest transformers "
                f"release ({snapshot['pypi_version']}).",
            }
        extra_packages, blockers = compat_plan(version)
        if blockers:
            return {
                "success": False,
                "version": version,
                "message": "Cannot install transformers "
                f"{version}: this environment does not satisfy {', '.join(blockers)}. "
                "A Studio update is required first.",
            }
        _is_installing = True

    try:
        success = ensure_latest_transformers_venv(version, extra_packages)
    finally:
        with _install_lock:
            _is_installing = False

    if not success:
        return {
            "success": False,
            "version": version,
            "message": f"Installing transformers {version} failed; see the Studio logs.",
        }
    return {
        "success": True,
        "version": version,
        "message": f"Installed transformers {version} into the latest sidecar "
        f"(pinned: {latest_venv_pinned_version()}).",
    }
References
  1. Ensure that long-running network operations, such as package installations, are executed outside of locked sections of code to prevent blocking critical operations like unload or cancellation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3c36aa8: installs are serialized with an install lock plus in-progress flag; a concurrent consent gets a structured already-in-progress refusal and the pip run happens outside the locked section.

Comment on lines +76 to +78
_lock = threading.Lock()
_memory_snapshot: dict | None = None
_last_failure_at: float = 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Define _install_lock and _is_installing to safely serialize concurrent installation requests, and _is_fetching to track active background fetches. This prevents race conditions and redundant network/installation calls.

Suggested change
_lock = threading.Lock()
_memory_snapshot: dict | None = None
_last_failure_at: float = 0.0
_lock = threading.Lock()
_install_lock = threading.Lock()
_memory_snapshot: dict | None = None
_last_failure_at: float = 0.0
_is_fetching: bool = False
_is_installing: bool = False

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 3c36aa8 (_install_lock, _is_fetching, _is_installing), and clear_caches resets the flags for tests.

Comment on lines +245 to +264
def latest_transformers_supports(model_type: str) -> dict | None:
"""Whether the newest transformers (PyPI release and/or GitHub main) ships *model_type*.

Returns ``{"pypi_version": str, "supported_in_pypi": bool, "supported_in_main": bool}``
or None when the answer is unavailable (offline, kill switch, network failure) — the
caller must then fall through to current behavior. Cached (memory + JSON snapshot on
disk, ttl ~1 day) so repeated tier resolutions never re-fetch.
"""
if not isinstance(model_type, str) or not model_type:
return None
if _disabled() or _env_offline():
return None
snapshot = _get_snapshot()
if snapshot is None:
return None
return {
"pypi_version": snapshot["pypi_version"],
"supported_in_pypi": model_type in set(snapshot["pypi_model_types"]),
"supported_in_main": model_type in set(snapshot["main_model_types"]),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Converting pypi_model_types and main_model_types lists to sets on every single call to latest_transformers_supports is inefficient. Caching the set-converted versions on the snapshot dictionary avoids this O(N) overhead.

def latest_transformers_supports(model_type: str) -> dict | None:
    """Whether the newest transformers (PyPI release and/or GitHub main) ships *model_type*.

    Returns ``{"pypi_version": str, "supported_in_pypi": bool, "supported_in_main": bool}``
    or None when the answer is unavailable (offline, kill switch, network failure) — the
    caller must then fall through to current behavior. Cached (memory + JSON snapshot on
    disk, ttl ~1 day) so repeated tier resolutions never re-fetch.
    """
    if not isinstance(model_type, str) or not model_type:
        return None
    if _disabled() or _env_offline():
        return None
    snapshot = _get_snapshot()
    if snapshot is None:
        return None
    if "_pypi_set" not in snapshot:
        snapshot["_pypi_set"] = set(snapshot["pypi_model_types"])
    if "_main_set" not in snapshot:
        snapshot["_main_set"] = set(snapshot["main_model_types"])
    return {
        "pypi_version": snapshot["pypi_version"],
        "supported_in_pypi": model_type in snapshot["_pypi_set"],
        "supported_in_main": model_type in snapshot["_main_set"],
    }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sets are ~670 strings and are consulted only when a model_type is absent from every installed overlay, so the conversion cost is negligible. The suggested mutation would also inject non-JSON-serializable sets into the snapshot dict that gets persisted to disk, breaking the cache write. Leaving as is.

Comment on lines +419 to +425
for raw in reqs:
try:
req = Requirement(raw)
except InvalidRequirement:
continue
if req.extras or (req.marker is not None and not req.marker.evaluate()):
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

req.marker.evaluate() can raise exceptions (such as UndefinedEnvironmentName or other packaging-specific errors) if the marker is malformed or contains unexpected variables. Catch these exceptions and log them at a debug level to aid in troubleshooting, rather than silently ignoring them.

    for raw in reqs:
        try:
            req = Requirement(raw)
        except InvalidRequirement:
            continue
        try:
            if req.extras or (req.marker is not None and not req.marker.evaluate()):
                continue
        except Exception as e:
            import logging
            logging.getLogger(__name__).debug("Failed to evaluate marker for %s: %s", raw, e)
            continue
References
  1. When handling exceptions, avoid broad except Exception: pass clauses. Instead, catch specific exceptions and log them (at least at a debug level) to aid in troubleshooting.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified in this environment: packaging's marker.evaluate() returns False rather than raising for extras and platform markers (tested extra == "testing", platform_system == "Windows"), and malformed markers cannot reach evaluate() because Requirement() raises InvalidRequirement first, which is caught. The crash path is not reachable with the pinned packaging version.

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 10, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c36aa8392

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +191 to +195
requires_transformers_upgrade: bool = Field(
False,
description = "True when the model's architecture is unknown to every installed "
"transformers but a newer transformers ships it; the UI should offer the "
"install-latest-transformers consent dialog (or the dev-only notice).",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire the upgrade prompt into the frontend

This response flag is never consumed by the chat UI: a repo-wide search only finds these new backend/test references, while the validation flows still branch only on requires_trust_remote_code / requires_security_review (for example studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts:599-602, shared-composer.tsx:958-961, and chat-adapter.ts:1383-1386), and the frontend ValidateModelResponse type has no upgrade fields. For a model whose model_type is only in latest transformers, /validate can return requires_transformers_upgrade=true, but the client ignores it and proceeds to /load, so users never see the consent/install dialog this field describes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate scoping, stated in the PR description: the frontend consent dialog ships in a follow-up PR (the exact wiring points and copy are specified in the code and PR body). The new validate field is additive and inert until then, so no current flow changes behavior; the backend endpoint is fully functional and testable on its own.

Comment on lines +4097 to +4100
from utils.transformers_latest import check_upgrade_for_model
_upgrade = await asyncio.to_thread(
check_upgrade_for_model, config.identifier, request.hf_token
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check LoRA base models for upgrade needs

When validating a LoRA adapter, this only checks config.identifier (the adapter path/repo), but the inference worker resolves adapter_config.json and activates transformers for the base model before loading. If the adapter's base model is the brand-new architecture, _load_config_json on a local adapter usually returns None (or reads only the adapter), so /validate never reports the upgrade requirement and the subsequent worker load still fails on the unsupported base. Include config.base_model (similar to the security target set above) in the upgrade check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7320811: the upgrade check now iterates the same [adapter, base] target set the security checks use, so a LoRA whose base model is a brand-new architecture surfaces the prompt.

Comment on lines +914 to +916
"550": _VENV_T5_550_DIR,
"510": _VENV_T5_510_DIR,
"latest": _VENV_T5_LATEST_DIR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore unpinned latest sidecars during mapping lookup

This lets _config_model_types("latest") read any .venv_t5_latest/transformers directory even when the pin marker is missing, but activation later refuses tier == "latest" unless latest_venv_pinned_version() is present. If an install succeeds for transformers but fails before writing the marker (or leaves a manual/partial directory), routing can select latest and check_upgrade_for_model can suppress the install prompt, while every load then fails with the unpinned-sidecar RuntimeError. Gate the latest overlay mapping on a valid pin, as _probe_tier_order() already does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7320811: _overlay_transformers_dir now returns None for latest when the pin marker is missing, matching activation and _probe_tier_order, so a partial or manual dir can neither be routed to nor suppress the install prompt.

Comment on lines +510 to +517
snapshot = _get_snapshot()
if snapshot is None:
return {
"success": False,
"version": version,
"message": "Could not verify the latest transformers release on PyPI.",
}
if version != snapshot["pypi_version"]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Reverify PyPI during install

The install path claims the requested version is rechecked against the current latest release, but _get_snapshot() first returns any in-memory or disk snapshot younger than 24 hours. If PyPI publishes a newer transformers after validation but before the cache expires, this endpoint still accepts and pins the stale version, so the sidecar may miss the newest architecture/fixes even though the server reports it installed “latest”. Force a live refresh (or bypass the TTL) before comparing version here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7320811: the install endpoint now attempts a live snapshot refresh before the version guard and only falls back to the cached snapshot when the fetch fails, so a release published inside the TTL is caught.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c0886c01b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +290 to +292
model_type = cfg.get("model_type")
if isinstance(model_type, str) and model_type:
return model_type

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check nested config types before suppressing upgrade

When a config has an already-supported wrapper at the top level (for example model_type: "llava") but a brand-new nested text_config.model_type or vision_config.model_type, this returns only the wrapper. check_upgrade_for_model() then sees the wrapper in the installed CONFIG_MAPPING and returns no upgrade signal, even though those wrapper configs instantiate sub-configs through CONFIG_MAPPING and will fail on the nested type; users with those multimodal repos still proceed to load and hit the unsupported-architecture error instead of getting the latest-transformers prompt.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the check now collects every model_type in the config (top level plus each nested sub-config, including vision_config) and signals on the first one missing from all installed overlays, with tests for a supported wrapper carrying a brand-new backbone and for the all-known no-signal case.


# Tier precedence: higher rank wins in _higher_tier. "latest" (the user-consented
# newest-release sidecar) outranks every fixed tier.
_TIER_RANK = {"default": 0, "530": 1, "550": 2, "510": 3, "latest": 4}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route vision detection through latest sidecar

Once this makes latest a loadable tier, latest-only VLMs can be installed and routed, but the capability detector still builds its VLM type/class sets from the ambient default transformers and its subprocess hard-codes .venv_t5_550 in utils/models/model_config.py. For a PyPI-latest VLM whose raw config is not caught by the old heuristics, validation/load can now select the latest sidecar while ModelConfig.from_identifier still reports is_vision=False, sending the backend down the text FastLanguageModel path and rejecting image requests; the vision probe needs to use the selected/latest tier too.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the vision capability subprocess now uses the pinned latest sidecar when the model resolves to the latest tier (static probe=False resolution), so latest-only VLMs classify correctly; every other tier keeps the 5.5 sidecar and current behavior.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b097d2b47a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +352 to +354
# Known to some installed overlay -> current routing handles it.
if any(candidate in _config_model_types(tier) for tier in _TIER_RANK):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route nested latest-only configs through latest

For configs where a supported wrapper is the top-level model_type but a nested sub-config is latest-only (for example model_type: "llava" with a new text_config.model_type), this check treats the nested type as handled once it appears in the installed latest overlay. The actual routing path still calls get_transformers_tier() / _tier_from_config_mapping(), which selects from the top-level type when present, so it stays on the default tier and the subsequent load can still fail on the nested architecture after the user installs latest. The routing decision needs to scan the same nested candidates before suppressing the upgrade path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 55d1466: _tier_from_config_mapping now routes by every model_type in the config via the shared collector, so a nested type raises the tier and a wrapper with a latest-only backbone routes to latest once installed. An unknown nested type never vetoes (no installed tier could load it; the checker surfaces the prompt), and the primary type keeps its previous semantics.

Comment on lines +734 to +736
from utils.transformers_version import _VENV_T5_LATEST_DIR, get_transformers_tier
if get_transformers_tier(model_name, hf_token, probe = False) == "latest":
sidecar_dir = _VENV_T5_LATEST_DIR

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Probe latest-only VLMs after raw vision miss

For a readable latest-only VLM whose config lacks the older raw heuristics (vision_config, known VLM class/type), _raw_config_has_vision_config returns False and _is_vision_model_uncached returns that value before this subprocess is called, so this new latest-sidecar selection never runs. Fresh evidence that the previous vision-routing concern remains is that the raw-config fast path still short-circuits the AutoConfig probe; after installing latest, those models are still cached as is_vision=False and image requests go down the text-model path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 55d1466: when the raw heuristics return False for a model that statically routes to the latest tier, the AutoConfig subprocess now runs under the pinned latest sidecar and its definitive answer wins; heuristic False stands for every other tier, so current behavior is unchanged.

and _venv_dir_is_valid(_VENV_T5_LATEST_DIR, packages)
):
return True
if not _ensure_venv_dir(_VENV_T5_LATEST_DIR, packages, f"transformers {version} (latest)"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the previous latest sidecar on upgrade failure

When a working .venv_t5_latest is already pinned and PyPI advances, this call reuses _ensure_venv_dir, which deletes the target directory before installing the requested package set. If the new install or subsequent marker write fails (network interruption, disk error, incompatible wheel), the old pinned sidecar has already been removed, so models that loaded with the previous latest tier stop working even though the upgrade did not succeed. Install into a temporary directory and swap it into place only after all packages and the marker are ready.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 55d1466: provisioning is now stage-and-swap. The new sidecar builds in .venv_t5_latest.staging and swaps in only after the install and pin marker complete, with the old dir restored if the final swap fails; a regression test covers upgrade failure preserving the previous pinned sidecar.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6f9c25a2f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +334 to +339
support = latest_transformers_supports(candidate)
if support is not None and (
support["supported_in_pypi"] or support["supported_in_main"]
):
model_type = candidate
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't prompt when the primary type stays unsupported

When the top-level model_type is unknown to both installed transformers and latest PyPI/main, but a nested text_config/vision_config type is supported by latest, this loop skips over the unsupported primary candidate and returns an upgrade prompt for the nested one. Installing latest still cannot load that repo because _tier_from_config_mapping() returns None as soon as types[0] is unknown, so the model continues down the default route and fails on the unsupported wrapper; only surface the prompt when the primary type is also loadable by the target latest tier.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c90b9c1: check_upgrade_for_model now collects every missing model_type first and prompts only when the latest release supports all of them, primary included. A nested-only match returns None since the install could not make the model loadable. Covered by test_upgrade_check_requires_primary_supported and test_upgrade_check_requires_every_missing_type.

Comment on lines +735 to +736
if get_transformers_tier(model_name, hf_token, probe = False) == "latest":
sidecar_dir = _VENV_T5_LATEST_DIR

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Build the vision probe predicates from the selected tier

When this branches to the latest sidecar, the subprocess can parse a latest-only config, but its _is_vlm helper still uses _VLM_MODEL_TYPES and _VLM_CLASS_NAMES inlined from the parent process's default transformers registry. For latest-only VLMs whose raw JSON lacks vision_config/image-token heuristics and whose only vision signal is the new transformers VLM mapping/class, the probe still returns False, so ModelConfig.from_identifier caches is_vision=False and image requests go through the text path even after installing latest. Fresh evidence is that only sidecar_dir changes here; the detection sets are not rebuilt under that sidecar.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c90b9c1: the vision-check subprocess now unions the active sidecar's own MODEL_FOR_IMAGE_TEXT_TO_TEXT and VISION_2_SEQ mapping names (plus the audio mappings) into the inlined sets before classifying, so architectures only the sidecar registry knows are detected. Covered by test_vision_subprocess_unions_sidecar_registry.

Comment thread studio/backend/routes/inference.py Outdated
Comment on lines +4208 to +4211
result = await asyncio.to_thread(install_latest_transformers, request.version)
if not result["success"]:
raise HTTPException(status_code = 400, detail = result["message"])
return InstallLatestTransformersResponse(**result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear pre-install vision cache entries after installing latest

A latest-only VLM is first validated before this endpoint runs, and that pre-install validation can cache is_vision=False because the raw-config heuristics return false while get_transformers_tier(..., probe=False) is not yet latest. After the install succeeds here, nothing invalidates _vision_detection_cache, so the follow-up validate/load reuses the stale non-vision result and never reaches the new latest-sidecar probe; clear capability caches (or include the pinned latest version in the key) when the sidecar changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c90b9c1: a successful install now clears the tier probe cache, drops the latest tier's cached model_type mapping, and clears the vision-detection cache, so the new sidecar takes effect without a restart. Covered by test_install_success_invalidates_capability_caches.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 500570158c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +340 to +342
if any(
s is None or not (s["supported_in_pypi"] or s["supported_in_main"]) for s in supports
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require PyPI support for all missing model types

When a config has multiple unknown model_types, this condition accepts a mix where the primary type is in the latest PyPI release but a nested sub-config type is only on transformers main. Because the response then copies supported_in_pypi from missing[0], the UI can offer/install the PyPI sidecar even though CONFIG_MAPPING will still fail on the main-only nested type after installation. Treat the installable path as valid only when every missing type is supported in PyPI, or surface the aggregate as main-only.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 83210e7: the signal now aggregates over every missing type, so supported_in_pypi is true only when the release covers all of them, and a mix with a main-only nested type surfaces as dev-only (no install offered). Covered by test_upgrade_check_mixed_pypi_main_reports_dev_only.

Comment thread studio/backend/routes/inference.py Outdated
)


@router.post("/install-latest-transformers", response_model = InstallLatestTransformersResponse)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move package install endpoint off the OpenAI router

Because this endpoint is registered on router, it is mounted under both /api/inference and the OpenAI-compatible /v1 prefix (main.py mounts inference_router in both places, while studio_router is explicitly kept off /v1). In deployments where API keys are shared with external OpenAI-compatible clients, any /v1 client can trigger a server-side pip install into .venv_t5_latest, which is a Studio consent/admin operation rather than an OpenAI API operation; register it on studio_router or otherwise keep it off /v1.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 83210e7: the endpoint now registers on studio_router, which main.py mounts only under /api/inference, so it is no longer reachable through the /v1 OpenAI-compatible mount. Covered by test_install_endpoint_not_mounted_on_v1.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 493665305e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +914 to +915
if tier == "latest" and latest_venv_pinned_version() is None:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the latest-transformers kill switch in routing

When UNSLOTH_STUDIO_NO_LATEST_TRANSFORMERS=1 is set after .venv_t5_latest has already been provisioned, this gate still exposes the latest overlay because it only checks for a pin marker. get_transformers_tier() can therefore keep selecting latest for latest-only model types, so operators cannot use the kill switch to roll back a problematic latest-transformers sidecar without deleting files manually. Please include the disabled env check wherever the latest tier is admitted into mapping/probe routing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2e42592: both admission points (_overlay_transformers_dir and _probe_tier_order) now check the kill switch in addition to the pin, so setting UNSLOTH_STUDIO_NO_LATEST_TRANSFORMERS rolls a provisioned sidecar out of routing without file deletion. Covered by test_kill_switch_removes_provisioned_latest_from_routing.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9155fc8dc4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

)
return False
marker_text = json.dumps({"version": version, "packages": list(packages)})
ok = _ensure_venv_dir(_VENV_T5_LATEST_DIR, packages, f"transformers {version} (latest)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the latest pin when repair fails

The explicit upgrade path is stage-and-swap now, but this lazy repair path still calls _ensure_venv_dir on the live .venv_t5_latest directory, and that helper deletes the directory before reinstalling. If an already pinned sidecar is considered incomplete and the repair install fails online, the marker is only rewritten on ok, so the pinned version is lost and future activations no longer know what to repair; use a staging repair or restore the marker/live dir on failure.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dc30b6d: the consented install's stage-and-swap body is now a shared helper and the lazy repair goes through it too, so a failed repair leaves the incomplete-but-pinned live dir and marker untouched for a later attempt. Covered by test_repair_failure_preserves_pin_and_live_dir; the existing repair test now asserts the staging path.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: dc30b6db06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 4cd9887122

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Base automatically changed from studio-static-transformers-router to main July 11, 2026 12:12
danielhanchen and others added 7 commits July 12, 2026 02:43
…ures

When a model's config.json model_type is absent from every installed
transformers overlay (base 4.57.x and the .venv_t5_530/550/510 sidecars),
Studio now checks, unauthenticated and cached, whether the newest
transformers ships it:

- utils/transformers_latest.py fetches the latest release version from
  https://pypi.org/pypi/transformers/json and the CONFIG_MAPPING_NAMES
  sources for that tag and for main from raw.githubusercontent.com
  (never api.github.com), parsing them with the same AST extractor the
  static router uses (no code execution, no trust_remote_code). Results
  are cached in memory and in a JSON snapshot under studio_root()/cache
  with a one day ttl; fetches are bounded to 5s with one retry and a
  failure backoff, and offline mode or the new kill switch
  UNSLOTH_STUDIO_NO_LATEST_TRANSFORMERS=1 short-circuits to None.

- POST /api/inference/validate gains requires_transformers_upgrade plus
  a transformers_upgrade payload (model_type, pypi_version,
  supported_in_pypi, supported_in_main) so the frontend can raise the
  install consent dialog before /load, mirroring the existing
  remote-code consent flow. The check fires only when the model_type is
  unknown to all installed overlays and the hardcoded tier tables.

- POST /api/inference/install-latest-transformers provisions a new
  persistent .venv_t5_latest sidecar after user consent, pinned to the
  exact PyPI version (re-verified server-side) with the same
  --target/--no-deps recipe as the fixed sidecars. A JSON pin marker
  inside the dir records the installed package set, so restarts
  revalidate it and routing resolves the new highest-ranked tier
  automatically. A dependency preflight (compat_plan) compares the
  release's requires_dist against the running env: unsatisfied
  tokenizers/safetensors floors are shadow-installed as exact pins into
  the sidecar, anything else unsatisfied blocks the install with a
  clear message.

Routing for every already-supported model_type is unchanged: the
hardcoded lists and the 530/550/510 static resolver run first, the new
tier only participates once its venv exists, and the probe order gains
the latest sidecar only when provisioned. Verified against live PyPI
and GitHub (transformers 5.13.0: 674 model_types, 26 absent from all
installed overlays, e.g. cosmos3_omni; 4 dev-only on main) and with a
real sidecar install plus restart persistence. 64 new tests; the
existing 200-test transformers_version suite passes unchanged.
Release the module lock during the network refresh so a slow fetch cannot
stall other threads in the ASGI pool; concurrent callers during a fetch get
None (the graceful fallthrough) via an in-flight flag instead of stacking
fetches. Serialize install_latest_transformers with an in-progress flag so
concurrent consents cannot race the sidecar delete and recreate; the loser
gets a structured already-in-progress refusal.
Run the upgrade check over the [adapter, base] target set so a LoRA whose
base model is a brand-new architecture surfaces the prompt (the worker
activates transformers for the base, not the adapter).

Gate the latest overlay's mapping lookup on a valid pin marker, matching
activation and the probe order, so a partial or manual .venv_t5_latest dir
cannot be routed to and then refused at activation.

Re-verify the requested version against a live PyPI snapshot at install
time, falling back to the cached one on fetch failure, so a release
published inside the cache TTL is not silently missed.
… probe

Collect every model_type in the config (top level plus each nested
sub-config) and signal on the first one missing from all installed
overlays, so a supported wrapper carrying a brand-new backbone still
surfaces the upgrade prompt; wrappers instantiate sub-configs through
CONFIG_MAPPING and would fail on the nested type.

Route the vision capability subprocess through the pinned latest sidecar
when the model resolves to the latest tier, so latest-only VLMs are not
misclassified as text-only; every other tier keeps the 5.5 sidecar used
today.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 9625835a57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Reclaim a swap lock as soon as its recorded owner PID is dead instead of
waiting out the two-hour cutoff, so a crash mid-install no longer wedges
/load, training, export, and repair for hours. A lock whose PID cannot be
read yet still uses the long cutoff so the create-before-write window is
never mistaken for dead.

Probe process liveness with OpenProcess on Windows: os.kill(pid, 0) there
is CTRL_C_EVENT (a real Ctrl+C via GenerateConsoleCtrlEvent), not a
harmless check, and psutil is not always present.

Return whether _shutdown_subprocess actually killed the worker and keep the
live handle when it survives terminate/kill (an uninterruptible CUDA
syscall can outlive SIGKILL). The pre-swap liveness guard now trusts that
result, so the destructive .venv_t5_latest rename cannot proceed while a
live worker still holds sidecar modules.

Recover a sidecar stranded at .old when a swap's activation rename and its
rollback both fail: reading the pin restores it when no swap holds the
reservation, so latest-tier models are not permanently broken.

Resolve the latest tier in the parent for export loads and for explicitly
16-bit training runs, not only 4-bit ones: tier resolution self-heals an
incomplete sidecar, and repairs are parent-only, so those paths could not
recover before. Sidecar integrity and quantization are independent.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcbd094787

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/routes/training.py Outdated
Comment on lines +362 to +366
latest_active = await asyncio.to_thread(
latest_tier_active_for,
training_kwargs["model_name"],
training_kwargs["hf_token"] or None,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Repair the latest sidecar after freeing chat

When a pinned .venv_t5_latest is incomplete and a chat worker is loaded, this preflight call tries the parent-only repair before _free_vram_for_training() has a chance to unload that chat worker. _workers_active_for_repair() then refuses the repair, latest_active falls back to false, and start_training() can continue into a worker child that is not allowed to repair the sidecar, so a latest-only training job fails after the route has already started the VRAM-freeing path. Move the repair check after the planned chat/export teardown or return a retryable 409 when the repair is blocked by resident workers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct: the probe ran before _free_vram_for_training unloaded the resident chat worker, so _workers_active_for_repair refused the parent-only repair, latest_active fell back to false, and start_training still tore the worker down and spawned a child that cannot repair. Reverted in f899119 so training returns to its prior reviewed form. Repairing correctly needs the repair to run between the worker teardown and the child spawn (decoupled from VRAM sizing); that is a larger change tracked as a follow-up rather than a partial probe that cannot complete while a worker is resident.

Comment thread studio/backend/routes/export.py Outdated
# early-returns on hosts with no sidecar pin, so this is a cheap no-op normally.
from utils.transformers_version import latest_tier_active_for

await asyncio.to_thread(latest_tier_active_for, request.checkpoint_path, request.hf_token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Repair latest sidecar after export teardown

When .venv_t5_latest is pinned but incomplete and an export checkpoint is already loaded, this parent-side repair probe runs while the old export worker is still alive, so _workers_active_for_repair() refuses the repair and the false result is ignored. load_checkpoint() then tears down the old worker and spawns a child that cannot perform parent-only repairs, causing latest-only checkpoint loads to fail after discarding the previously loaded export state; retry the repair after unloading the export worker, or refuse before teardown when repair is blocked.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, same root cause as the training probe: an already-loaded export worker makes _workers_active_for_repair refuse the parent-only repair, and load_checkpoint then tears the old worker down and spawns a child that cannot repair. Reverted in f899119 so export returns to its prior reviewed form; the teardown-then-repair fix is tracked as a follow-up.

Comment on lines +4059 to +4061
if effective_load_in_4bit and not config.is_gguf:
from utils.transformers_version import latest_tier_active_for
if await asyncio.to_thread(latest_tier_active_for, config.identifier, request.hf_token):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Repair latest sidecar for 16-bit chat loads

This parent-side latest-tier check is only run while the effective request is still 4-bit. If .venv_t5_latest is pinned but incomplete and the chat load is already 16-bit (for example a full-precision LoRA/base or a direct API caller with load_in_4bit=false), the parent never attempts the only allowed repair path; load_model() can then tear down the current worker and spawn a multiprocessing child that refuses parent-only repairs, so the latest-only model fails to load and the previous chat model may already be gone. Run the latest-tier/repair probe for all non-GGUF loads, using the result only to flip 4-bit when needed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct that this probe is 4-bit-gated and the inference 16-bit path has the same coupling. Not changing it in this PR though: hoisting it would hit the identical resident-worker problem (load_model tears down the current worker, so the probe's parent-only repair is refused while that worker is still live, then the child cannot repair either). The correct fix for all three load paths is to run the repair between the worker teardown and the child spawn; tracking that as a single follow-up rather than adding a partial probe here that cannot complete while a worker is resident.

…t loads

The probe ran before the route freed VRAM, so a resident chat or export worker
made _workers_active_for_repair() refuse the parent-only repair; the route then
tore that worker down and spawned a child that also cannot repair, so an
incomplete sidecar still failed to load. Repairing correctly requires running the
repair between the worker teardown and the child spawn, decoupled from VRAM
sizing, which is a larger change tracked separately. Restore the prior behavior
so these paths match the reviewed form and do not partially attempt a repair that
cannot complete while workers are resident.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

except RuntimeError as exc:
op_success, op_message = False, str(exc)
return False, str(exc), None

P3 Badge Propagate sidecar-swap export refusals as 409

When an export request races a latest-transformers install or repair, the SidecarSwapInProgress raised above is immediately caught here because it subclasses RuntimeError, so _run_export returns (False, ...) and the route responds through its normal 400 path instead of the retryable 409 handlers added for this condition. This only affects export operations started during the sidecar swap window, but it makes clients treat a transient retry case as a bad export request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 1082 to 1086
if tier == "latest" and _latest_tier_disabled():
return frozenset()
cached = _config_mapping_cache.get(tier)
if cached is not None:
return cached

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate cached latest mappings before routing

When .venv_t5_latest has already been parsed once, this early cache return bypasses _overlay_transformers_dir("latest"), so the new _latest_sidecar_intact() repair path is never reached if the pinned sidecar is later deleted or loses a pinned package in the same Studio process. In that state /validate and /load can keep routing latest-only architectures to the stale cached latest tier, while worker activation/probing sees the broken sidecar and the model load fails until the process restarts or the cache is manually cleared.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 80d000d. _config_model_types now drops and re-resolves the cached latest mapping when _latest_sidecar_intact() is false, so a sidecar deleted or broken in-process after its first parse re-enters _overlay_transformers_dir (which self-heals) instead of routing latest-only models to a stale tier until restart. Non-latest tiers keep the plain cache path. Covered by TestCachedLatestMappingRevalidated.

Comment on lines +282 to +289
if self._proc is not None and self._proc.is_alive():
# Survived SIGKILL (uninterruptible syscall): keep the handle so callers
# and the pre-swap guard see a live worker rather than a nulled one.
logger.error(
"Inference subprocess still alive after terminate/kill; "
"preserving its handle for the pre-swap liveness check"
)
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor failed worker shutdowns before replacing handles

This new False result is meant to preserve a worker that survived terminate/kill, but the fresh-load paths still ignore it before spawning a replacement (for example InferenceOrchestrator.load_model continues after _shutdown_subprocess() and _spawn_subprocess overwrites self._proc). When a CUDA worker is wedged in the exact scenario this return value handles, starting another model loses the only handle to the survivor, so is_worker_alive() and the sidecar pre-swap guard can no longer see it while the old process still holds GPU memory/modules.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 80d000d. Both fresh-load paths now honor the False result: InferenceOrchestrator.load_model raises before spawning, and ExportOrchestrator.load_checkpoint returns a retryable failure, when _shutdown_subprocess reports the old worker survived. So a wedged worker's handle is no longer overwritten by a replacement, is_worker_alive/the pre-swap guard keep seeing it, and the load can retry once it exits. Covered by test_load_model_aborts_when_old_worker_survives_shutdown and TestSpawnPathsHonorFailedShutdown.

…t mapping

The fresh-load paths spawned a new worker straight after _shutdown_subprocess
without checking its result, so a worker that outlived terminate/kill (a wedged
CUDA syscall) had its handle overwritten by the replacement while it still held
GPU memory, and is_worker_alive/the pre-swap guard could no longer see it. Both
the inference load and the export checkpoint load now abort when the old worker
did not exit, so the load can be retried once it does.

_config_model_types returned a cached latest mapping without re-checking the
sidecar, so a sidecar deleted or broken in-process after its first parse was
never re-validated: routing kept sending latest-only models to the stale latest
tier while activation failed. The cached latest mapping is now dropped and
re-resolved (self-healing) when the sidecar is no longer intact.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 80d000d6ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…-transformers

# Conflicts:
#	studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b4db8ae30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +961 to +964
pin = _latest_pin_data()
if pin is None:
# No valid pin means nothing to repair against (callers gate on the pin).
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat missing latest pin as stale

When .venv_t5_latest or just its pin marker is deleted after the latest mapping has already been cached, this returns True, so _config_model_types("latest") keeps returning the stale cached model types instead of dropping the cache and re-resolving through _overlay_transformers_dir. In that same-process scenario /validate will no longer offer reinstall/repair and workers can still route the model to latest, then fail activation because latest_venv_pinned_version() is None, leaving users stuck until restart or manual cache clearing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in db02372. _latest_sidecar_intact now returns False when the pin marker itself is gone (not only when a pinned package is missing), so _config_model_types drops the cached latest mapping and re-resolves to no latest tier instead of routing latest-only models to a tier that then fails worker activation with no pinned version. The _overlay_transformers_dir caller already gates on a present pin, so it is unaffected. Covered by test_deleted_pin_drops_cached_latest_mapping.

Comment thread studio/backend/routes/inference.py Outdated
Comment on lines +4671 to +4674
if (
transformers_upgrade is not None
and transformers_upgrade.supported_in_pypi
and transformers_upgrade.pypi_version

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve 4-bit sizing for custom-code fallback

When a model both has an installable PyPI upgrade and can fall back to its own auto_map code, this branch sizes validation as 16-bit before the trust/custom-code checks run. Because the install route refuses to proceed while training is active, a user with active training whose GPU can fit the custom-code 4-bit load but not the latest-sidecar 16-bit load gets a 409 from /validate and never reaches the fallback dialog, even though /load without a successful install would keep load_in_4bit enabled.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in db02372. The offered-upgrade 16-bit flip in validate_model is now gated on the absence of a custom-code fallback (not requires_trust_remote_code, resolved before the flip). A model with auto_map loads 4-bit on the current transformers exactly as /load does without a successful install, and the install route refuses while training is active, so 16-bit sizing here no longer 409s the only viable 4-bit path. An already-active latest sidecar still always sizes 16-bit, and /load re-sizes 16-bit after a successful install and re-guards there. Covered by test_validate_offered_upgrade_preserves_custom_code_4bit.

…m-code fallback

_latest_sidecar_intact now returns False when the pin marker itself is gone, not
just when a pinned package is missing. Otherwise a cached latest mapping outlived
a deleted pin: _config_model_types kept returning it, so routing sent latest-only
models to a tier whose worker activation then failed (no pinned version) until
restart. It now drops the cache and re-resolves to no latest tier. The
_overlay_transformers_dir caller already gates on a present pin, so it is
unaffected.

validate_model forced 16-bit sizing whenever a PyPI upgrade was merely offered,
even for a model that can fall back to its own auto_map code. /load loads such a
model 4-bit without the install, and the install route refuses while training is
active, so 16-bit sizing here returned a VRAM 409 for the only viable 4-bit path.
The offered-upgrade flip is now gated on the absence of a custom-code fallback;
an already-active latest sidecar still always sizes 16-bit.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 14, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: db02372a1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…-transformers

# Conflicts:
#	studio/backend/core/training/training.py
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 15, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 16da8d0cca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danielhanchen
danielhanchen merged commit 815f242 into main Jul 15, 2026
53 of 54 checks passed
@danielhanchen
danielhanchen deleted the studio-dynamic-latest-transformers branch July 15, 2026 12:25
danielhanchen added a commit that referenced this pull request Jul 15, 2026
Resolve start_training conflict: keep main's spawn-tracking refactor (#7056)
and re-apply the progress-log throttle reset inside the new reset-state block.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant