Skip to content

Studio: multi-select export formats, portable FP8/INT8, GGUF LoRA, and source parity#6767

Merged
wasimysaid merged 24 commits into
mainfrom
studio-export-formats-dropdown
Jul 3, 2026
Merged

Studio: multi-select export formats, portable FP8/INT8, GGUF LoRA, and source parity#6767
wasimysaid merged 24 commits into
mainfrom
studio-export-formats-dropdown

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Reworks the Studio export page into a single, cross-platform export flow.

  • One sorted, multi-select precision list. The merged formats are unified into one list sorted by bit width (16-bit first, then 8-bit, then 4-bit). Labels drop the "vLLM" suffix, and the set is expanded: 16-bit, FP8, INT8 (W8A8), INT8 (W8A16), INT4 (W4A16), MXFP4, MXFP8, NVFP4, plus the two portable formats below. The common ones are quick toggle pills; the rest live in a multi-select "More formats" dropdown. Several formats can be selected and exported in one run, each to its own sibling directory.
  • Portable FP8 / INT8 that needs no NVIDIA GPU. Adds a torchao save path (Float8WeightOnlyConfig / Int8WeightOnlyConfig) that quantizes on any device, including CPU, AMD, and Apple. FP8 serializes to safetensors and INT8 to .bin; both load in vLLM. This is wired into save_pretrained_merged / push_to_hub_merged through a TORCHAO_EXPORT_SCHEMES registry and a new _unsloth_save_torchao, parallel to the existing compressed-tensors path.
  • Hardware-aware gating. When no NVIDIA GPU is present, the NVIDIA-only compressed-tensors formats are hidden and only 16-bit plus the portable FP8/INT8 remain. The backend also rejects a compressed request on non-NVIDIA hardware, so it stays authoritative even if the UI is bypassed.
  • Source parity for Local Model and Hugging Face. The merged export is relaxed to non-PEFT models, so base-model sources get the same 16-bit / compressed / portable options that fine-tuned adapters do. LoRA-only export stays gated to real adapters.
  • GGUF multi-quant in one call. The whole quant list is sent in a single request, so the model is merged to 16-bit once and every GGUF is produced from that merge instead of re-merging per quant.
  • GGUF LoRA adapters. LoRA-only export gains a "GGUF adapter" option (llama.cpp convert_lora_to_gguf.py, loadable with llama-cli --lora) with an output-type select (f16/bf16/f32/q8_0/auto), alongside the safetensors adapter.

How it works

The precision pills and the "More formats" dropdown both toggle into one selectedFormats list. Each selected format maps to a backend payload: 16-bit is a plain save, a compressed alias (fp8, w4a16, mxfp4, ...) resolves via unsloth.save.COMPRESSED_EXPORT_SCHEMES, and a portable alias (torchao_fp8, torchao_int8) resolves via TORCHAO_EXPORT_SCHEMES. The store loops the merged selections (one call per format) and sends the GGUF quant list in a single call. NVIDIA presence comes from useHardwareInfo() (cuda != null && rocm == null) in the UI and from DEVICE == CUDA and not IS_ROCM in the backend. The new fields are threaded through the request models, routes, orchestrator, and worker.

Verification

  • Portable torchao FP8 and INT8 quantized on pure CPU (no GPU), then loaded and generated in vLLM (B200): both produce coherent output.
  • Studio ExportBackend, driven with a real Llama-3.2-1B LoRA: torchao FP8 merged (sibling -torchao-fp8, safetensors), the NVIDIA guard (compressed rejected on a simulated non-NVIDIA box, portable still allowed), the relaxed non-PEFT merged export, GGUF multi-quant (Q4_K_M + Q8_0 in one call), and GGUF LoRA (*-lora-f16.gguf) all succeed.
  • End-to-end over HTTP against the running Studio (routes to orchestrator to worker to backend): torchao FP8 merged export returns success and writes the checkpoint.
  • Backend export tests extended and passing (28); frontend tsc -b clean and eslint adds no new errors; Vite build clean; live UI drive confirms the multi-select pills, the "More formats" dropdown, the GGUF LoRA option, and GGUF multi-quant all render and toggle.

Export gating (PyTorch / accelerator)

Export runs through Unsloth, which requires a compute accelerator (NVIDIA/AMD/Intel GPU or Apple MLX) and has no CPU code path, so a bare-CPU host cannot export even with PyTorch installed (Unsloth calls torch.cuda unconditionally at import). A new hardware.export_capability() reports export_supported plus a precise reason so the UI stops showing a generic "no GPU":

  • pytorch_not_installed: a --no-torch install, where even a physical GPU is unusable without PyTorch.
  • no_accelerator: PyTorch present but no supported accelerator (bare CPU).
  • mlx_unavailable: Apple Silicon where the MLX stack is missing or too old.

The fields are exposed on /api/system/hardware and /api/system. The mutating export routes reject up front with HTTP 400 and the reason, while read-only endpoints stay usable so the Export page still renders. core/export/export.py now imports without PyTorch and without a usable accelerator (the Unsloth import is caught), so the export worker degrades to a clear message instead of crashing at import. On the frontend the Export page stays reachable on chat-only hosts and grays out the method and format options with the backend reason (an alert plus disabled pickers) instead of silently redirecting to chat.

This also fixes the export save directory producing "model/null" for Local Model and Hugging Face sources that have no run/checkpoint.

Cross-platform verification

  • CPU-only isolated venv (CPU-only PyTorch, no accelerator): export_capability() correctly reports no_accelerator, and an export attempt returns the clear message without crashing. This confirms Unsloth cannot export on bare CPU (it fails at torch.cuda init), so the gate blocks it honestly instead of letting a user click into a runtime crash.
  • New workflow studio-export-capability-ci.yml runs the capability and import-safety tests on ubuntu-latest, windows-latest and macos-latest. Hosted runners have no GPU or MLX, so this validates the "export unavailable, here is why" path on real Windows and macOS; a real accelerator export is validated on GPU hardware.
  • Backend capability tests (11) and the full export test set pass locally; tsc -b clean; eslint adds no new errors.

@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 a broader range of vLLM compressed-tensors schemes by adding a compressed_method parameter to the model merging and export pipeline. This allows the frontend to expose a 'More formats' dropdown (including FP8, INT, MXFP, and NVFP4) that overrides the default format type. The backend has been updated to resolve these schemes dynamically against the Unsloth registry. A critical issue was identified in the backend export logic where the unresolved compressed_alias is assigned to save_method instead of the resolved base save method, which would cause the export to fail.

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 thread studio/backend/core/export/export.py Outdated
Comment on lines +467 to +468
elif is_compressed:
save_method = compressed_alias

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

The save_method should be set to the resolved base save method (info[0]) from the registry lookup rather than the unresolved compressed_alias (which could be an alias like 'fp8_static' or 'w4a16'). Passing the unresolved alias directly to save_pretrained_merged will cause the export to fail as it is not a recognized save method in Unsloth.

Suggested change
elif is_compressed:
save_method = compressed_alias
elif is_compressed:
save_method = info[0]

danielhanchen and others added 2 commits July 1, 2026 04:13
…LoRA, source parity

Export page overhaul on top of the formats dropdown:

- Unify merged precision into one sorted multi-select list (16-bit first, then
  8-bit, then 4-bit). Drop "vLLM" from labels, add INT8 (W8A8), INT8 (W8A16),
  INT4 (W4A16), MXFP4, MXFP8. Quick formats render as toggle pills; the rest live
  in a multi-select "More formats" dropdown, so several formats export in one run.
- Add a portable torchao FP8/INT8 save path (Float8WeightOnlyConfig /
  Int8WeightOnlyConfig) that needs no NVIDIA GPU to produce and loads in vLLM.
  FP8 serializes to safetensors, INT8 to .bin. Wired into save_pretrained_merged
  and push_to_hub_merged via a TORCHAO_EXPORT_SCHEMES registry and
  _unsloth_save_torchao, parallel to the compressed-tensors path.
- Hide NVIDIA-only compressed-tensors formats when no NVIDIA GPU is present; keep
  16-bit and portable FP8/INT8. The backend also rejects a compressed request on
  non-NVIDIA hardware so it stays authoritative.
- Relax merged export to non-PEFT models so Local Model and Hugging Face sources
  get the same 16-bit / compressed / portable options.
- GGUF: send the whole quant list in one call (merge once, quantize many).
- LoRA: add a GGUF adapter option (convert_lora_to_gguf.py) with an outtype
  select (f16/bf16/f32/q8_0/auto), alongside the safetensors adapter.
- Thread the new fields through models, routes, orchestrator, and worker; extend
  the export tests.
@danielhanchen danielhanchen changed the title Studio: expose the full compressed-tensors scheme set in an export formats dropdown Studio: multi-select export formats, portable FP8/INT8, GGUF LoRA, and source parity Jul 1, 2026

@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: 0b3e52f025

ℹ️ 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 unsloth/save.py Outdated
Comment on lines +4672 to +4674
if not push_to_hub and os.path.isdir(local_dir):
try:
shutil.rmtree(local_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.

P1 Badge Do not rmtree the user-selected export directory

For local torchao exports, local_dir is the caller's save_directory, not an isolated temp dir. This cleanup therefore recursively deletes the user-selected output directory after quantization; if the user picked an existing directory, or selected 16-bit plus a portable torchao format in the new multi-format UI, it removes the 16-bit export and any pre-existing files. Use a temporary staging directory or only delete a directory that this path created solely for staging.

Useful? React with 👍 / 👎.

Comment thread unsloth/save.py
Comment on lines +4640 to +4644
quantized_model = auto_model.from_pretrained(
local_dir,
device_map = "auto",
quantization_config = TorchAoConfig(quant_type = quant_type),
**dtype_kw,

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 Pass trust_remote_code when reloading torchao exports

When exporting a model whose config has auto_map/custom code, the UI can approve and load it with trust_remote_code=True, but the torchao path merges to local_dir and then reloads the staged checkpoint here without forwarding that trust. AutoModel...from_pretrained will reject those local staged checkpoints after the expensive merge, so portable FP8/INT8 exports fail for remote-code models; mirror the compressed path's trust handling for the model and processor reloads.

Useful? React with 👍 / 👎.

Comment on lines +201 to +203
const availableFormats = useMemo<MergedFormatOption[]>(
() => MERGED_FORMATS.filter((f) => hasNvidia || !f.needsNvidia),
[hasNvidia],

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 Hide portable quant formats on MLX

On macOS/MLX, hardware.cuda and hardware.rocm are both null, so this filter keeps every needsNvidia:false option, including torchao_fp8 and torchao_int8. The backend rejects is_torchao when _IS_MLX, so the page advertises portable FP8/INT8 formats that always fail on Apple Silicon; either detect MLX and hide them or allow the backend path.

Useful? React with 👍 / 👎.

Comment on lines +461 to 464
compressed_method: sel.compressedMethod,
push_to_hub: pushToHub,
repo_id: params.repoId,
hf_token: params.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 Do not upload every selected format to the same Hub repo

When destination is Hub and a user selects more than one merged format, this loop sends each export to the same repo_id. All formats save root-level files like config.json and model shard names, so later uploads overwrite or mix with earlier ones and the Hub repo cannot contain all selected formats; use per-format repo IDs/subfolders or disable multi-select for Hub.

Useful? React with 👍 / 👎.

Comment on lines +1214 to +1215
: !isAdapter
? ["lora"]

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 Load full-checkpoint merged exports in full precision

For sourceMode === 'checkpoint' and a full fine-tune (!isAdapter), this now leaves Merged enabled, but the checkpoint load path still omits load_in_4bit:false and therefore uses the backend default true. The non-PEFT merged save then serializes the 4-bit-loaded model rather than a true 16-bit export, so full-checkpoint merged exports can be quantized or wrong; load non-adapter merged checkpoints in full precision like local/HF sources.

Useful? React with 👍 / 👎.

Comment thread unsloth/save.py Outdated
Comment on lines +4607 to +4609
is_vlm = any(
x.endswith(("ForConditionalGeneration", "ForVisionText2Text")) for x in archs
)

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 Do not treat every ConditionalGeneration model as VLM

For portable torchao exports of text/audio encoder-decoder models such as T5, BART, or Whisper, their architecture names end in ForConditionalGeneration, so this marks them as VLMs and reloads the staged checkpoint with AutoModelForImageTextToText/AutoProcessor. Those models are not image-text models and the reload fails after the merge; match the narrower VLM test used by the compressed path (vision_config or explicit vision architecture) instead.

Useful? React with 👍 / 👎.

Comment on lines +64 to +65
import unsloth.save as _us
return hasattr(_us, "_normalize_torchao_method")

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 Verify torchao is actually usable before enabling export

In environments where Studio intentionally has no real torchao package, such as Windows ROCm where the installer skips torchao and installs a runtime stub, unsloth.save still defines _normalize_torchao_method, so this reports portable export support and bypasses the NVIDIA compressed-tensors guard. The export then fails later when _unsloth_save_torchao imports or calls stubbed torchao classes; check for a real torchao.quantization implementation (or explicitly block the skipped/stubbed platforms) before classifying aliases as torchao.

Useful? React with 👍 / 👎.

…ort save dir naming

Export runs through Unsloth, which requires a compute accelerator (NVIDIA/AMD/Intel
GPU or Apple MLX) and has no CPU code path, so a bare-CPU host cannot export even
with PyTorch installed. Add export_capability() in utils/hardware that reports
export_supported plus a precise reason so the UI stops showing a generic "no GPU":

  - pytorch_not_installed: a --no-torch install (even a physical GPU is unusable)
  - no_accelerator: PyTorch present but no supported accelerator (bare CPU)
  - mlx_unavailable: Apple Silicon where the MLX stack is missing or too old

Expose the fields on /api/system/hardware and /api/system, and guard the mutating
export routes (load-checkpoint, export/merged|base|gguf|lora) with HTTP 400 and the
reason, leaving read-only endpoints usable so the Export page still renders.

Make core/export/export.py import without PyTorch and without a usable accelerator
(the Unsloth import is caught) so the export worker degrades to a clear message
instead of crashing at import.

Frontend: keep /export reachable on chat-only hosts and gray out the method and
format options with the backend reason (Alert plus disabled MethodPicker) instead
of silently redirecting to /chat, so users see why export is unavailable.

Also fix the export save directory producing "model/null" for Local Model and
Hugging Face sources that have no run/checkpoint, naming the folder from the model id.
Add a small pytest matrix that runs studio/backend/tests/test_export_capability.py
on ubuntu-latest, windows-latest and macos-latest. It confirms, on each real OS,
that hardware.export_capability() reports the right decision and reason
(pytorch_not_installed, no_accelerator, or mlx_unavailable) and that the export
backend imports without PyTorch and degrades to a clear message instead of crashing.

Hosted runners have no GPU/MLX, so this covers the "export unavailable, here is why"
path a Mac/Windows user without an accelerator sees; a real accelerator export is
validated separately. The job installs only a CPU PyTorch plus the backend import
deps (no unsloth, triton, or llama.cpp), so it runs in seconds with no GPU.

@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: 1ed2989636

ℹ️ 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 +1401 to +1406
<Button
type="button"
variant={loraAsGguf ? "default" : "outline"}
size="sm"
onClick={() => setLoraAsGguf(true)}
title="llama.cpp GGUF LoRA, loadable with `llama-cli --lora`."

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 Hide GGUF LoRA on MLX

On Apple Silicon/MLX, export_lora_adapter explicitly returns an error when gguf is true (studio/backend/core/export/export.py checks _IS_MLX before conversion), but this new option still lets users set loraAsGguf whenever export is otherwise supported. As a result, MLX users can choose the GGUF adapter path, wait through the load, and then always fail; gate this button on the same MLX condition or add backend support.

Useful? React with 👍 / 👎.

Comment on lines +1254 to +1256
: !isAdapter
? ["lora"]
: []

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 Gate LoRA on the active source

When the user switches to a Local/Hugging Face source after selecting a LoRA fine-tuned run, isAdapter still reflects the old selectedModelData, not the direct model being exported. This leaves LoRA enabled for an unrelated direct base model, but runExport loads that direct source with isAdapter: false and the backend then rejects it with “No adapter to export”; include sourceMode/the active source in this gate instead of relying on stale fine-tuned-run metadata.

Useful? React with 👍 / 👎.

Comment on lines +1076 to +1081
self.current_model.save_pretrained_gguf(
save_directory,
self.current_tokenizer,
save_method = "lora",
quantization_method = outtype,
)

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 Pass the HF token into LoRA GGUF conversion

For GGUF LoRA exports whose adapter/base is gated or private, the load can succeed with the Studio-provided hf_token, but this new conversion call drops that token. The LoRA GGUF helper uses the token argument to set HF_TOKEN for convert_lora_to_gguf.py when it fetches the base config, so these exports fail even though the user supplied a valid token; pass token = hf_token here.

Useful? React with 👍 / 👎.

Comment on lines +1252 to +1253
: !isAdapter && isQuantized
? ["merged", "lora", "gguf"]

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 block direct exports with stale quantized state

If a user first selects a quantized full fine-tune run and then switches to a Local/Hugging Face source, isQuantized still comes from the old selectedModelData. This branch therefore disables every export method for an unrelated direct source, even when that source is exportable; only apply the quantized-checkpoint restriction while sourceMode === "checkpoint" (or derive it from the active source).

Useful? React with 👍 / 👎.

Comment thread studio/backend/core/export/export.py Outdated
f"Choose one of {', '.join(_GGUF_LORA_OUTTYPES)}.",
None,
)
if not _supports_kwarg(self.current_model.save_pretrained_gguf, "save_method"):

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 for save_pretrained_gguf before inspecting it

On an older Unsloth model that lacks save_pretrained_gguf, this attribute access raises before _supports_kwarg can return False and before the method's try block starts. That turns the intended clean “does not support GGUF LoRA adapter export” response into a generic 500 from the route; use getattr or catch AttributeError in this guard.

Useful? React with 👍 / 👎.

…oken/MLX/guard)

Frontend (export-page):
- Gate LoRA and quantized-model restrictions on the active source. isAdapter /
  isQuantized come from the selected checkpoint; in Local Model / Hugging Face
  ("model") source mode they were stale, so LoRA stayed wrongly enabled for a
  direct base model (backend then rejects "No adapter to export") and a stale
  "quantized" flag disabled every method for an unrelated, exportable model. Add
  effectiveIsAdapter / effectiveIsQuantized (false outside checkpoint mode) and use
  them in the method-reset effect and the MethodPicker disabled state.
- Hide the GGUF LoRA option on a macOS/MLX host (the backend rejects GGUF LoRA on
  MLX), so users no longer pick it, wait through the load, and always fail. Disable
  the "GGUF adapter" button on a Mac host and never send loraGguf there.

Backend (core/export/export.py):
- Pass the HF token into the GGUF LoRA conversion (save_pretrained_gguf), so a
  gated/private base model's config fetch in convert_lora_to_gguf.py is
  authenticated; without it the load can succeed but the conversion fails.
- Guard the save_pretrained_gguf capability check with getattr so an older Unsloth
  model that lacks the method returns the clean "not supported" message instead of
  an AttributeError that surfaces as a generic 500.
@danielhanchen

Copy link
Copy Markdown
Member Author

Thanks, all five are real. Fixed in 5df1ad5.

  1. Gate LoRA on the active source: isAdapter/isQuantized come from the selected checkpoint, so in Local Model / Hugging Face (model) source mode they were stale. Added effectiveIsAdapter/effectiveIsQuantized (false outside checkpoint mode) and use them in the method-reset effect and the MethodPicker disabled state, so LoRA is no longer wrongly enabled for a direct base model.

  2. Don't block direct exports with stale quantized state: same fix, effectiveIsQuantized is only true in checkpoint mode, so a stale quantized flag no longer disables every method for an unrelated, exportable direct source.

  3. Hide GGUF LoRA on MLX: the GGUF adapter button is now disabled on a macOS host (device_type from /api/health), with an explanatory title, and loraGguf is never sent there, so MLX users don't pick it and then fail after the load.

  4. Pass the HF token into LoRA GGUF conversion: save_pretrained_gguf(..., token = hf_token or None) so convert_lora_to_gguf.py can authenticate a gated/private base model's config fetch (it sets HF_TOKEN from token).

  5. Check for save_pretrained_gguf before inspecting it: the capability check now uses getattr, so an older Unsloth model lacking the method returns the clean 'not supported' message instead of an AttributeError that surfaced as a 500.

tsc clean, eslint adds no new errors (8 -> 8 baseline), 50 backend export tests pass.

@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: 5df1ad54d0

ℹ️ 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".

- name: Install CPU PyTorch
# Pin to the CPU wheel index so every OS gets a CPU build (Windows
# would otherwise pull a CUDA wheel from PyPI; macOS has no CUDA wheel).
run: python -m pip install --index-url https://download.pytorch.org/whl/cpu "torch>=2.4,<2.13"

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 Add PyPI as an extra index for torch dependencies

On fresh runners this replaces the default PyPI index with only the PyTorch CPU index. I checked python -m pip install --help: --index-url is the base package index, while --extra-index-url is for indexes used in addition, so torch's transitive dependencies are also resolved only from download.pytorch.org here. Other workflows in this repo add --extra-index-url https://pypi.org/simple; without that, this new capability workflow can fail during the torch install step before any tests run.

Useful? React with 👍 / 👎.

quantLevels,
useImatrix: effectiveImatrix,
mergedFormat,
mergedSelections: selectedFormats.map((v) => mergedFormatPayload(v)),

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 Do not submit empty merged-format selections

If the panel is already open, a user can deselect every precision pill; canExport only disables the outer Export Model button, but the panel's Start Export button still calls handleStart. This sends mergedSelections: [], and export-runtime-store.ts falls back to a default 16-bit export for an empty list, so a user who selected no formats still launches an expensive 16-bit export; mirror the merged selectedFormats.length guard in handleStart or prevent unchecking the last format.

Useful? React with 👍 / 👎.


def test_unsloth_save_has_torchao_registry_and_path():
# The core normalizer + registry + save path must exist for the backend to route to them.
import unsloth.save as us

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 Keep backend tests free of direct Unsloth imports

The regular studio-backend-ci.yml backend job runs python -m pytest tests/ from studio/backend without installing the repo package or adding the repo root to PYTHONPATH; I also checked that import unsloth.save from that working directory raises ModuleNotFoundError. This new test is therefore picked up by the CPU backend suite and fails during the torchao-registry check, even though the surrounding file otherwise uses AST/string checks to avoid the heavy Unsloth import path.

Useful? React with 👍 / 👎.

… import)

- studio-export-capability-ci.yml: add --extra-index-url https://pypi.org/simple to
  the torch install so torch's transitive deps still resolve; --index-url alone
  replaces PyPI with only the CPU wheel index, which does not serve all of them.
- export-page handleStart: reject an empty merged selection (mirrors canExport), so
  clicking the panel's Start button with every precision pill deselected no longer
  submits mergedSelections: [] and launches an unintended default 16-bit export.
- test_export_imatrix_compressed: the torchao-registry test now reads unsloth/save.py
  as text (like the other ast/string checks) instead of `import unsloth.save`, which
  raised ModuleNotFoundError in the CPU studio-backend suite that has no unsloth
  installed.
@danielhanchen

Copy link
Copy Markdown
Member Author

Thanks, all three fixed in 45f3df8.

  1. Add PyPI as an extra index for torch deps: the capability workflow's torch install now passes --extra-index-url https://pypi.org/simple alongside --index-url .../whl/cpu, so torch's transitive deps still resolve (matching the other workflows here).

  2. Don't submit empty merged-format selections: handleStart now returns early when exportMethod === "merged" and selectedFormats.length === 0 (mirrors canExport), so clicking the panel's Start button with every precision pill deselected no longer submits an empty list that falls back to a default 16-bit export.

  3. Keep backend tests free of direct Unsloth imports: the torchao-registry test now reads unsloth/save.py as text (like the other ast checks in the file) instead of import unsloth.save, so it no longer raises ModuleNotFoundError in the CPU studio-backend suite. Verified the file's 28 tests pass without unsloth on the path.

tsc clean, eslint adds no new errors (8 -> 8 baseline).

@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: 45f3df8af7

ℹ️ 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 thread unsloth/save.py Outdated
Comment on lines +4672 to +4674
if not push_to_hub and os.path.isdir(local_dir):
try:
shutil.rmtree(local_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.

P1 Badge Avoid removing the user-selected export directory

When a local torchao export runs, local_dir is the caller's save_directory, so this rmtree deletes the entire user-selected folder after producing the -torchao-* sibling. That wipes any pre-existing files in a custom export directory, and it also removes the 16-bit artifact when the new multi-select flow exports 16-bit before a portable FP8/INT8 format using the same base path. Use a temporary staging directory (or only remove files created for staging) instead of recursively deleting the requested output path.

Useful? React with 👍 / 👎.

Comment on lines 462 to 463
push_to_hub: pushToHub,
repo_id: params.repoId,

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 upload multiple merged formats to one Hub repo

When Destination is Hub and more than one merged precision is selected, this loop submits every format to the same repo_id. The Hub upload paths write model files at the repository root, so a 16-bit upload followed by FP8/INT8 can overwrite config.json/weights and leave stale shards from the previous format, producing a repo that is inconsistent or unloadable. Use a per-format repo/path or restrict Hub merged exports to one selected format.

Useful? React with 👍 / 👎.

"""True if the installed unsloth build has the portable torchao FP8/INT8 export path."""
try:
import unsloth.save as _us
return hasattr(_us, "_normalize_torchao_method")

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 real torchao availability before routing aliases

This helper returns true as soon as unsloth.save contains the torchao registry, but the actual torchao package can still be unavailable or stubbed; for example, studio/install_python_stack.py skips torchao on Windows ROCm and the export worker installs the stub from core/_torchao_stub.py. In that environment the UI/backend accept torchao_fp8/torchao_int8, then _unsloth_save_torchao fails later when it tries to import/use the quantization classes. Verify the real torchao classes are importable (or disable these aliases on the stubbed platform) before classifying the alias as supported.

Useful? React with 👍 / 👎.

Comment on lines +24 to +27
except Exception as _unsloth_exc: # ImportError (e.g. missing torch) or a broken native load
FastLanguageModel = None
FastVisionModel = None
_IS_MLX = 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 Return the cached import error before loading

Because this broad catch also nulls FastLanguageModel/FastVisionModel for a broken Unsloth native load, a GPU host still passes the new export_capability() route guard, then load_checkpoint() later dereferences these None model classes and reports a cryptic load failure. Check _export_runtime_available() at the start of load_checkpoint() (or surface _UNSLOTH_IMPORT_ERROR) so a broken Unsloth install fails with the actual import problem instead of a NoneType error.

Useful? React with 👍 / 👎.

@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: 299f40c36b

ℹ️ 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 +490 to +492
hf_token: params.token,
private: params.privateRepo,
gguf: params.loraGguf ?? 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 Forward the load token for local GGUF LoRA exports

When loraGguf is true for a local export of a gated/private adapter or base, this request still sends hf_token: params.token, but handleStart only populates params.token for Hub destinations while the load uses loadToken. Fresh evidence after the earlier backend fix: the backend now forwards hf_token into save_pretrained_gguf(... token=...), but this new GGUF LoRA call still supplies the hub-only token, so local GGUF LoRA conversion runs without the user’s token and fails when it fetches the gated base config; pass params.loadToken here when not pushing to Hub.

Useful? React with 👍 / 👎.

@danielhanchen

Copy link
Copy Markdown
Member Author

Agreed, valid P2. Fixed in d9409ba.

handleStart sets token only for Hub pushes (pushToHub && hfToken ? hfToken : undefined) but always sets loadToken, while the LoRA export call sent hf_token: params.token. So a local GGUF LoRA export of a gated/private base ran with no token and the base-config fetch during conversion failed. The LoRA call now sends params.token ?? params.loadToken ?? null, so it authenticates on a local export and still uses the Hub token when pushing (both derive from the same HF token). tsc clean.

@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: d9409ba63c

ℹ️ 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".

hardware.exportUnsupportedMessage ??
"Export requires a supported accelerator (NVIDIA, AMD, or Intel GPU, or Apple Silicon) with PyTorch or MLX installed.";
const availableFormats = useMemo<MergedFormatOption[]>(
() => MERGED_FORMATS.filter((f) => hasNvidia || !f.needsNvidia),

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 Hide torchao formats on MLX hosts

On an Apple Silicon/MLX host, exportUnsupported is false and hasNvidia is false, so this filter keeps every format with needsNvidia === false, including torchao_fp8 and torchao_int8. The backend rejects those same requests in export_merged_model when _IS_MLX is true (Quantized ... export is not supported on macOS/MLX), so the UI advertises portable FP8/INT8, lets the user start the export, and then fails after loading. Filter torchao formats out for MLX/macOS here, or make the backend support them.

Useful? React with 👍 / 👎.

Comment thread unsloth/save.py Outdated
Comment on lines +4657 to +4659
if not push_to_hub and os.path.isdir(local_dir):
try:
shutil.rmtree(local_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 Preserve 16-bit output when torchao follows it

When the user leaves the default 16-bit format selected and adds a portable FP8/INT8 format, the runtime writes the 16-bit export to save_directory first and then runs torchao with that same save_directory as its staging directory. This cleanup deletes local_dir after quantization, so the previously selected 16-bit artifact is removed even though multi-select promises each selected format as its own output. Use an isolated staging directory for torchao or avoid deleting save_directory when it may be a selected output.

Useful? React with 👍 / 👎.

Comment thread unsloth/save.py
Comment on lines +4626 to +4630
quantized_model = auto_model.from_pretrained(
local_dir,
device_map = "auto",
quantization_config = TorchAoConfig(quant_type = quant_type),
**dtype_kw,

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 Forward trust_remote_code during torchao reload

For an HF/local source that required approved custom code to load, the initial load can succeed with trust_remote_code, but the portable torchao path reloads the staged checkpoint here without passing that flag. Checkpoints with auto_map are rejected by Transformers unless trust_remote_code=True, so torchao FP8/INT8 exports fail after the merge for exactly the custom-code models the UI already approved; derive the flag from model.config.auto_map as the compressed path does and pass it to this reload.

Useful? React with 👍 / 👎.

Comment on lines 459 to 460
repo_id: params.repoId,
hf_token: params.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 Avoid uploading every selected format to one repo

When destination === "hub" and the user selects multiple merged formats, this loop calls the backend once per format but reuses the same repo_id every time. Each call uploads a full model/config to the repository root, so later formats overwrite or mix with earlier ones (for example 16-bit plus torchao INT8 leaves one repo with incompatible root-level artifacts) instead of producing separate outputs. Either restrict Hub export to one merged format or derive a per-format repo/path.

Useful? React with 👍 / 👎.

Comment thread unsloth/save.py Outdated
Comment on lines +4594 to +4596
is_vlm = any(
x.endswith(("ForConditionalGeneration", "ForVisionText2Text")) for x in archs
)

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 Do not classify all ConditionalGeneration models as VLMs

For portable torchao exports, any architecture ending in ForConditionalGeneration is treated as a vision model and reloaded with AutoModelForImageTextToText. Text seq2seq/audio models such as T5/BART/Whisper also use *ForConditionalGeneration, so they will be sent through the image-text auto class and fail during FP8/INT8 export; the compressed path below already avoids this by checking vision_config or ForVisionText2Text only. Match that narrower VLM detection here.

Useful? React with 👍 / 👎.

… push

torchao (_unsloth_save_torchao):
- merge to an isolated temp staging dir so a co-selected 16-bit output at save_directory is not deleted
- narrow VLM detection to vision_config / ForVisionText2Text so T5/BART/Whisper are not misrouted
- forward trust_remote_code (from auto_map) to the reload so custom-code models export

Export UI:
- hide portable torchao formats on macOS/MLX (backend rejects quantized export there)
- restrict a Hub merged export to a single format (each writes to the repo root)
@danielhanchen

Copy link
Copy Markdown
Member Author

All five valid - fixed in fce8c9a.

Backend (unsloth/save.py, _unsloth_save_torchao)

  • Preserve 16-bit when torchao follows it: the torchao merge now goes to an isolated temp staging dir instead of save_directory, so a co-selected 16-bit export at save_directory is no longer overwritten/deleted. The torchao output stays the <dir>-<suffix> sibling; the staging dir is cleaned in finally (the local rmtree(save_directory) step is gone).
  • Don't treat all ConditionalGeneration models as VLMs: narrowed detection to vision_config / *ForVisionText2Text only, matching the compressed path, so T5/BART/Whisper reload via AutoModelForCausalLM instead of AutoModelForImageTextToText.
  • Forward trust_remote_code on reload: derived from model.config.auto_map (like the compressed path) and passed to both from_pretrained calls, so approved custom-code models don't fail after the merge.

Frontend (export-page.tsx)

  • Hide torchao on MLX hosts: availableFormats now drops backend === "torchao" when isMacHost, so a Mac no longer advertises portable FP8/INT8 that the backend rejects (_IS_MLX).
  • Avoid uploading every format to one repo: a Hub merged export is restricted to a single format (each format writes to the repo root, so multiple would collide). Multi-format stays available for local export; canExport/handleStart gate it and an inline note explains why.

Verified: ruff clean, tsc clean, eslint unchanged (8 -> 8, all pre-existing), 39 backend + 34 saving tests pass.

@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: fce8c9a9ba

ℹ️ 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 thread unsloth/save.py
is_vlm = hasattr(model.config, "vision_config") or any(
x.endswith("ForVisionText2Text") for x in archs
)
trust_remote_code = bool(getattr(model.config, "auto_map", 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 Preserve tokenizer remote-code approval for torchao reloads

For portable torchao exports where the approved custom code is declared only in the tokenizer/processor config, this derives trust_remote_code from model.config.auto_map and leaves it false. Fresh evidence after the earlier trust fix is that the reload below also calls auto_processor.from_pretrained(..., trust_remote_code=trust_remote_code), while the consent scanner covers tokenizer/processor auto_map too; those exports will fail after the merge even though the user approved the code. Carry the original approval through, or inspect the staged tokenizer/processor configs as well.

Useful? React with 👍 / 👎.

Comment thread unsloth/save.py Outdated
Comment on lines +4610 to +4615
torch.cuda.is_available()
and hasattr(model, "parameters")
and not getattr(model, "is_loaded_in_4bit", False)
and not getattr(model, "is_loaded_in_8bit", False)
and not getattr(model, "is_quantized", 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 Offload XPU models before torchao reloads

On Intel/XPU hosts, the portable torchao formats remain available because they do not need NVIDIA, but this memory-release branch only runs for torch.cuda.is_available(). The original XPU model therefore stays resident while the export reloads a second quantized copy with device_map="auto", so models that fit once on XPU can OOM during FP8/INT8 export; include the XPU cache/offload path or move any single-device accelerator model to CPU before reloading.

Useful? React with 👍 / 👎.

const { outputPath } = await runRecoverableOp(() =>
exportGGUF({
save_directory: params.saveDirectory,
quantization_method: params.quantLevels,

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 per-quant timeout for multi-GGUF exports

When a user selects multiple GGUF quantizations for a large model, this sends the whole list as one export operation. The orchestrator still waits only 3600s per operation (core/export/orchestrator.py sets that timeout for export_gguf_done), so two or more 30B+ quantizations can time out even though the previous loop gave each quant its own hour. Either keep per-quant calls/recovery or scale the timeout for list exports.

Useful? React with 👍 / 👎.

…F timeout

torchao (_unsloth_save_torchao):
- honor auto_map in the staged tokenizer/processor configs (not just model.config) when
  deriving trust_remote_code, so custom-code tokenizers reload after the merge
- offload single-device XPU models to CPU (and empty the XPU cache) before the reload, matching
  the CUDA path, so an Intel GPU that fits the model once does not OOM on the second copy

Export orchestrator:
- scale the GGUF wait timeout by the number of requested quants so a multi-quant list export of a
  large model does not time out at a flat 3600s
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@danielhanchen

Copy link
Copy Markdown
Member Author

All three valid - fixed in cdafa38.

unsloth/save.py (_unsloth_save_torchao)

  • Tokenizer/processor remote-code approval: trust_remote_code was derived only from model.config.auto_map, so a model whose custom code is declared solely in the tokenizer/processor config would fail the reload. Now, when the model config has no auto_map, it also checks for auto_map in the staged config.json / tokenizer_config.json / processor_config.json / preprocessor_config.json (the load already had the user's consent) and passes the flag through to both reloads.
  • XPU offload: the pre-reload memory release only ran for CUDA, so on an Intel/XPU host the original model stayed resident while device_map="auto" loaded a second copy and could OOM. It now offloads a single-device CUDA or XPU model to CPU and empties the XPU cache too (restored in finally).

core/export/orchestrator.py

  • Per-quant GGUF timeout: a multi-quant list runs as one op off a single merge, but the wait was a flat 3600s, so several 30B+ quants could time out. The timeout now scales by the quant count (3600 * n); single-quant and non-GGUF exports are unchanged.

Verified: ruff clean, py_compile OK, 39 backend + 16 saving tests pass.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 6151dca788

ℹ️ 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".

… hosts

Portable torchao FP8/INT8 is the fallback for hosts without the NVIDIA compressed-tensors path.
On an NVIDIA GPU the compressed-tensors FP8/FP4/INT formats are the intended path (llm-compressor
auto-installs), so hide the portable duplicates there; keep them on CPU / non-NVIDIA hosts and
continue hiding them on macOS/MLX.
- Multi-format merged export now collects every sibling output directory (one per selected
  precision) instead of only the last; the success banner lists them all.
- Show the selected precision formats in the run summary (a Formats row, like GGUF Quantizations),
  so the panel says what is being exported rather than just 'Merged Model'.
- Persist the selected formats in the run summary and seed them on mount, so navigating away and
  back (or toggling the export method) restores the selection instead of resetting to 16-bit.

@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: 15d22c5d13

ℹ️ 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 thread unsloth/save.py Outdated
Comment on lines +4622 to +4623
auto_model = AutoModelForImageTextToText if is_vlm else AutoModelForCausalLM
auto_processor = AutoProcessor if is_vlm else AutoTokenizer

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 Use non-causal auto classes for torchao reloads

When the portable torchao path is used on supported non-causal checkpoints (for example Whisper, which the export load path handles with WhisperForConditionalGeneration), the narrowed VLM test now leaves is_vlm false and this branch reloads the staged checkpoint with AutoModelForCausalLM/AutoTokenizer. Fresh evidence after the earlier VLM fix is that T5/BART/Whisper no longer go to the image-text class, but they still hit the wrong causal-LM auto class here and fail after the merge; either carry the original model class/audio type through or hide torchao formats for those architectures.

Useful? React with 👍 / 👎.


Returns {export_supported, export_unsupported_reason, export_unsupported_message}.
"""
if get_device() in (DeviceType.CUDA, DeviceType.XPU, DeviceType.MLX):

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 Allow CPU hosts to use portable exports

On a CPU-only host with PyTorch installed, this new capability check still returns unsupported because DeviceType.CPU is excluded, and _ensure_export_supported() uses it to reject /load-checkpoint before any export starts. That blocks the new portable torchao FP8/INT8 flow the UI/constants describe as device-agnostic/CPU-capable (and also prevents a plain 16-bit local save) even though the torchao path no longer requires NVIDIA; allow the CPU+torch case and gate only formats that truly need an accelerator.

Useful? React with 👍 / 👎.

Comment thread unsloth/save.py Outdated
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
AutoModelForImageTextToText,

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 Fall back when image-text auto class is unavailable

On Transformers versions that do not expose AutoModelForImageTextToText, this unconditional import aborts every portable torchao export before the code even checks whether the model is a VLM, so plain text models fail too. Other Unsloth paths guard this class with an AutoModelForVision2Seq fallback; apply the same lazy/fallback import here so text-only FP8/INT8 exports continue to work on supported Transformers installs.

Useful? React with 👍 / 👎.

… Q8_0, auto-select newest checkpoint

- Progress/summary panel now shows a Formats row with the selected merged
  formats, and the success banner lists every output folder a multi-format
  merged run creates (one line per format) instead of only the last one.
- Merged format selection is seeded from the active run, so navigating away
  and back (or switching method cards) no longer resets it to 16-bit.
- GGUF / Llama.cpp now offers an Export target toggle (Full model or LoRA
  adapter) for adapter checkpoints, reusing the LoRA GGUF export path.
- Removed the Auto GGUF LoRA output type and defaulted to Q8_0 in the UI,
  the request model, and the backend defaults; the outtype list is now
  Q8_0/F16/BF16/F32. Core save.py still accepts auto for external callers.
- When a finetune has no checkpoint selected, auto-select the newest one.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@danielhanchen

Copy link
Copy Markdown
Member Author

Pushed export UX fixes from re-testing the page on a real finetune:

  • The run panel now shows a Formats row with the selected merged formats, and the success banner lists every output folder a multi-format run writes (one per format), not just the last one. Previously a multi-format merged export reported a single directory.
  • Merged format selection is seeded from the active run, so navigating away and back or switching method cards keeps the selection instead of resetting to 16-bit.
  • GGUF / Llama.cpp now has an Export target toggle (Full model or LoRA adapter) for adapter checkpoints, reusing the existing LoRA GGUF path.
  • Removed the Auto GGUF LoRA output type and defaulted to Q8_0 across the UI, the request model, and the backend defaults; the list is now Q8_0/F16/BF16/F32. Core save.py still accepts auto for external callers.
  • When a finetune has no checkpoint chosen, the newest checkpoint is auto-selected.

Verified end to end on a Llama-3.2-1B LoRA: tsc, eslint (no new errors), and the Vite build are clean. A merged 16-bit + FP8 run produced two output folders, both listed in the success banner with a Formats row of "16-bit, FP8". A GGUF LoRA export with the default output type produced a valid Q8_0 adapter, and gguf_outtype=auto is now rejected by the request model.

Two fixes to the portable torchao FP8/INT8 export reload, from review of the
narrowed VLM detection:

- Encoder-decoder seq2seq checkpoints (T5/BART/Whisper) are not causal LMs.
  With the narrowed is_vlm test they now correctly skip the image-text class,
  but fell through to AutoModelForCausalLM and failed to reload after the merge.
  Reload them with their own architecture class from the config instead.
- AutoModelForImageTextToText was imported unconditionally at the top of the
  torchao path, so on Transformers builds without that class the import aborted
  every torchao export (even text-only). Import it lazily only for a VLM, with
  the AutoModelForVision2Seq fallback used elsewhere in Unsloth.
@danielhanchen

Copy link
Copy Markdown
Member Author

Addressed the torchao review findings (pushed in 96d1a78):

  • Encoder-decoder seq2seq reload: T5/BART/Whisper are not causal LMs, and after the narrowed VLM detection they were falling through to AutoModelForCausalLM and failing to reload post-merge. They now reload with their own architecture class resolved from the config; decoder-only and VLM paths are unchanged.
  • Unconditional AutoModelForImageTextToText import: moved to a lazy import used only for a VLM, with the AutoModelForVision2Seq fallback used elsewhere in Unsloth, so text-only FP8/INT8 exports keep working on Transformers builds that lack that class.

On the CPU-hosts capability finding: declining that one. import unsloth raises NotImplementedError: Unsloth cannot find any torch accelerator? You need a GPU on a CPU-only host (unsloth_zoo resolves the device type at import), so there is no CPU export path today. Allowing CPU in export_capability would let /load-checkpoint start and then crash mid-export with that cryptic error instead of the current clean "no accelerator" message. CPU-only hosts already have the export page fully disabled, so there is no user-facing inconsistency. The portable torchao formats target non-NVIDIA accelerators (Intel XPU, AMD/ROCm, Apple MLX), which are reported as supported.

@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: 96d1a78ebb

ℹ️ 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".

from models.export import ExportLoRAAdapterRequest

r = ExportLoRAAdapterRequest(save_directory = "/tmp/x")
assert r.gguf is False and r.gguf_outtype == "f16"

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 Align GGUF LoRA default assertions with q8_0

This assertion is stale: ExportLoRAAdapterRequest now defaults gguf_outtype to q8_0, and the export worker fallback also uses q8_0, so the backend pytest suite will fail here before exercising the new GGUF LoRA path; the string check a few lines below has the same f16 expectation. Update these assertions, or change the implementation defaults, so CI can pass.

Useful? React with 👍 / 👎.

sourceMode === "checkpoint"
? checkpoint ?? selectedModelIdx ?? sourceBaseModelName
: sourceBaseModelName;
return `${safePathSegment(rawName)}-${exportMethod === "lora" ? "adapter" : "merged"}`;

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 Keep direct merged exports discoverable

When sourceMode === "model" and the newly enabled Merged method is used, this default writes a flat exports/<model>-merged directory. The exported-model scanner in studio/backend/utils/models/model_config.py only recognizes top-level directories as GGUF, then looks for merged config.json/weights one level deeper under a run/checkpoint directory, so a successful default Local/HF merged export never appears in the exported models list or Chat picker; use a scanner-supported layout or teach the scanner about flat merged exports.

Useful? React with 👍 / 👎.

The shipped llm-compressor 0.10.x pins transformers<=4.57.6, so FP8/FP4 export failed
for models needing a transformers 5.x sidecar (Qwen3.5, Gemma-4, Qwen3-Next): the
quantization subprocess crashed importing the removed TORCH_INIT_FUNCTIONS.

Run the quantization against a dedicated llm-compressor-main "shadow": a --target
package dir (transformers 5.10.2 + llm-compressor main + compressed-tensors) layered
over the existing torch. It installs --no-deps so torch is never touched (works on any
Studio torch build), is provisioned lazily and fingerprint-cached, and can be turned
off with UNSLOTH_DISABLE_LLMCOMPRESSOR_MAIN.

- transformers_version.py: provision + validate .venv_llmcompressor.
- export.py: route all compressed exports through the shadow when available; else keep
  the workspace 0.10.x path and fail fast past its transformers ceiling.
- save.py: launch _compressed_quantize.py with a clean PYTHONPATH = shadow.
- _compressed_quantize.py: skip linear_attn / vision tower / MTP modules (matches the
  RedHatAI and NVIDIA reference quants, and is required by the grouped schemes).

Verified all four schemes (fp8, w8a8, w4a16, mxfp4) on Qwen3.5-9B and Llama-3.2-1B, and
fp8 on Gemma-4, end to end through Studio.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@danielhanchen

Copy link
Copy Markdown
Member Author

Pushed 710e19e: FP8/FP4 compressed export now works for newer-transformers models (Qwen3.5, Gemma-4, Qwen3-Next), which previously failed because the shipped llm-compressor 0.10.x pins transformers<=4.57.6.

The quantization now runs against a dedicated llm-compressor-main shadow: a --target package dir (transformers 5.10.2 + llm-compressor main + compressed-tensors) provisioned lazily under the Studio home and layered over the existing torch. It installs --no-deps so torch is never touched, so it works on any Studio torch build; fingerprint-cached, and can be disabled with UNSLOTH_DISABLE_LLMCOMPRESSOR_MAIN. When the shadow is unavailable the workspace 0.10.x path is kept and fails fast past its transformers ceiling.

_compressed_quantize.py now skips the linear_attn, vision tower, and MTP modules, matching the RedHatAI and NVIDIA reference quants (and required by the grouped schemes, which otherwise error on the non group-divisible linear-attention shapes).

Verified end to end through Studio:

scheme Llama-3.2-1B Qwen3.5-9B Gemma-4
fp8 pass pass pass
w8a8 pass pass -
w4a16 pass pass -
mxfp4 pass pass -

Outputs match the reference module ignore lists (Qwen3.5 w4a16 keeps the linear_attn projections in bf16, same as RedHatAI/Qwen3.5-9B-quantized.w4a16). 16-bit and LoRA export are unchanged.

…-dropdown

# Conflicts:
#	studio/backend/main.py
#	unsloth/save.py
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@wasimysaid
wasimysaid merged commit 026141a into main Jul 3, 2026
61 of 62 checks passed
@wasimysaid
wasimysaid deleted the studio-export-formats-dropdown branch July 3, 2026 15:25
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.

2 participants