* Add FP8/FP4 compressed export to save_pretrained_merged
Adds compressed-tensors export (for vLLM) to save_pretrained_merged /
push_to_hub_merged via llm-compressor, alongside the existing lora /
merged_16bit / merged_4bit / gguf / torchao paths:
model.save_pretrained_merged("model", tokenizer, save_method="fp8")
Supported save_method values: fp8 (FP8_DYNAMIC), mxfp4, nvfp4 (W4A4) and
mxfp8. The LoRA is merged to 16bit at save_directory, then a quantized
checkpoint is written to save_directory + "-<fmt>". nvfp4 needs a small
calibration set (defaults to ultrachat, overridable via calibration_dataset).
Notes:
- llm-compressor is installed lazily on first use, pinning the current torch
and transformers via a constraints file so they are not upgraded (a plain
install pulls transformers>=5 and breaks Unsloth).
- Quantization runs in a separate process (unsloth/_compressed_quantize.py,
launched by file path) so Unsloth's transformers attention patches do not
interfere with the forward llm-compressor runs during calibration, mirroring
how GGUF export shells out to llama.cpp.
- mxfp8 needs a newer llm-compressor (transformers>=5); it is recognised and
raises a clear error until that stack is available.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Address review: main-process guard, calibration subsampling, tokenizer + dtype handling
- Route the 16bit merge through unsloth_generic_save for both LoRA and full
finetuned models, so non-PEFT models are written in 16bit consistently
instead of saving the original (possibly quantized) weights directly.
- Honor is_main_process: only the main process quantizes and writes the
compressed output, so distributed ranks do not race on the same dirs.
- Subsample an in-memory calibration Dataset before save_to_disk so large
training sets are not fully copied to a temp dir.
- Tolerate a missing tokenizer in the converter (data-free exports); still
require one for calibration based schemes.
- Open config.json via a context manager in both files.
- Drop the redundant nvfp4 entry from the unsupported-name check (fp4 covers it).
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add direct LoRA to GGUF export and harden FP8/FP4 compressed export
- Run llm-compressor install and scheme check before the 16bit merge so
unsupported schemes (e.g. mxfp8) fail fast without writing a checkpoint
- Only the main process installs, merges, quantizes and uploads; isolate
hub pushes to a temp dir and clean all temp dirs in a finally
- Forward standard save kwargs (state_dict, max_shard_size, ...) to the merge
- Fall back to the first dataset split for Hub calibration ids
- Export LoRA adapters to GGUF via convert_lora_to_gguf.py: modernize
save_pretrained_ggml/push_to_hub_ggml and add save_method="lora" to
save_pretrained_gguf/push_to_hub_gguf; resolve base from the adapter config
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Fix LoRA GGUF shell-injection test and compressed export trailing-slash path
- Update tests/saving/test_save_shell_injection.py for the new delegation: the
LoRA to GGUF conversion now lives in _unsloth_save_lora_gguf, so assert it
passes argv as a list with no shell=True and that the legacy ggml wrappers
delegate to it instead of calling subprocess.Popen directly
- Normalize the local save_directory before building the "<dir>-<fmt>" sibling
so a trailing slash no longer nests the compressed output inside the 16bit dir
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Polish FP8/FP4 and LoRA GGUF export after review
- Warn (not silently downgrade) when an explicit quantization_method is not a
valid LoRA GGUF outtype; default stays f16
- Correct the inference hardware note: MXFP8 is 8-bit (cc >= 8.9), only FP4
needs Blackwell for full activation quantization
- Document that a local fp8/fp4 save keeps the 16bit merge at save_directory
and writes the quantized checkpoint to save_directory + "-<fmt>"
* Use sequential calibration pipeline and validate Hub access early
- nvfp4 calibration no longer forces the memory-hungry "basic" pipeline. The
quantization runs in a clean subprocess, so llm-compressor's default
sequential pipeline (layer-by-layer onloading) works and lets large models
that do not fit at once still calibrate; fall back to "basic" only if tracing
fails
- For push_to_hub compressed exports, create/validate the repo up front so a bad
token or denied repo fails before the merge and quantization instead of after
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Harden compressed export: explicit sequential pipeline, base-tokenizer calibration, GPU memory
- nvfp4 calibration now passes pipeline="sequential" explicitly (layer-by-layer
onloading) instead of relying on the inferred default, with a "basic" fallback
- Calibration datasets with a messages column no longer require a chat template:
base / non-chat tokenizers fall back to concatenating message contents
- Free the in-memory model's CUDA memory before the quantize subprocess loads its
own copy from disk (best-effort, single-device non-quantized only; restored
afterward), so a single GPU need not hold two copies at once
- Create the calibration temp dir in the system temp location instead of next to
the save directory, avoiding stray dirs in the workspace
* Free the failed calibration model before the basic-pipeline retry
In the sequential -> basic NVFP4 fallback, release the partially-processed model
and clear the CUDA cache before loading a fresh copy, so the retry does not
transiently hold two model copies on the GPU.
* Harden calibration data handling and compressed-export edge cases
- Calibration messages without a chat template now handle multimodal (list)
content, None content, and null message rows instead of crashing on join
- Raise a clear error when the calibration dataset is empty after subsampling
- Reset llm-compressor's global session before freeing the model in the
sequential -> basic NVFP4 fallback, so the old model is actually released
- LoRA GGUF export accepts a single-element list quantization_method
- Attach datasets metadata to the pushed repo on compressed hub exports
- Warn (instead of silently) if the model cannot be restored to its device
- Raise a clear error if the LoRA base model id cannot be determined
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Handle DatasetDict calibration, MoE routers, and MTP models in compressed export
- Reduce an in-memory DatasetDict calibration set to a single split before row
subsampling, so save_to_disk does not copy every split to the temp dir
- For MoE models, keep the router/gate unquantized and pass
moe_calibrate_all_experts so every expert is calibrated
- Warn when a model carries MTP / speculative-decoding tensors that the
compressed export does not include
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Support many more compressed-tensors schemes and address review
- Expand save_method to cover the full set of compressed-tensors preset schemes:
FP8 (dynamic/static/block), INT8, W8A8, W8A16, W4A16(+asym), W4A8, W4AFP8,
MXFP4(+A16), NVFP4(+A16), plus the gated MXFP8; calibration is used only for
the static-activation schemes (FP8 static, NVFP4)
- Broaden the near-miss save_method error to cover int/w-prefixed names
- MoE: also keep the Qwen shared-expert gate unquantized
- Strip non-model-input columns from already-tokenized calibration data so the
collator does not choke on a leftover messages column
- Forward the Hub token to the LoRA converter and the quantize subprocess so
gated/private base models and calibration datasets work without a global login
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Collapse compressed-tensors export help line so ruff-format converges
The print line in print_quantization_methods needed two ruff-format passes to
reach a fixpoint (merge implicit string concat, then collapse the single-arg
print). pre-commit.ci applies one pass per run, so it kept reformatting. Land
the converged single-line form directly.
* Add CPU-only regression tests for the export API
Cover all export paths without a GPU, for slow CPU-only CI:
- pure-function checks of the compressed-tensors scheme registry and save_method
normalization (aliases, calibration flags, near-miss errors)
- AST checks that every merged saver dispatches compressed export, the GGUF savers
expose the lora branch, torchao routes PTQ/QAT, the public methods stay attached,
and the export subprocesses remain shell-safe (argv list, sys.executable, no shell)
- monkeypatched dispatch checks that fp8/nvfp4/merged_16bit, the LoRA-GGUF outtype
resolution, and torchao PTQ/QAT reach the right helper with the right arguments
* Run the CPU-only export tests in consolidated CI
tests/saving is --ignored by the Repo tests (CPU) job, so the new GPU-free export
tests are added by path to consolidated-tests-ci.yml (collection sanity + Bucket-A run),
alongside the existing CPU saving tests, so they actually execute on CPU CI.
* Add GPU GGUF export + llama-cli inference smoke test
tests/saving/test_gguf_export_and_inference.py: skipif no CUDA. Trains a tiny
phrase-imprinting LoRA, exports a full-model q8_0 GGUF (merge -> convert_hf_to_gguf
-> llama-quantize), asserts a valid GGUF (magic + size), and - when a llama-cli
binary is available - runs one bounded generation (byte cap + watchdog kill) and
asserts the trained phrase round-trips through HF -> GGUF -> quantize -> inference.
The llama-cli step skips gracefully since the export only builds llama-quantize.
* Fix variant mismatch in compressed (FP8/FP4) export
save_pretrained_merged(..., save_method=fp8/nvfp4, variant=...) forwarded
the variant into the intermediate 16bit merge, so Transformers wrote
variant-named shards (model.<variant>.safetensors). The converter
subprocess then reloaded that directory with the default weight filenames,
so the compressed export failed after doing the merge.
Pop the variant out of the intermediate merge (internal staging that the
subprocess reloads with default names) and forward it via --variant so it
is applied to the final compressed checkpoint instead. Add a CPU AST guard
for the contract.
* Harden export paths from review
- install_llm_compressor: fall back to uv pip when this interpreter has no
pip seeded (uv-created/relocatable venvs), instead of failing with
No module named pip.
- LoRA GGUF export: if convert_lora_to_gguf.py is missing (a prebuilt or
reused CWD llama.cpp install carries binaries but not the converter
script), force a dedicated source checkout that ships it.
- push_to_hub_gguf(save_method=lora): return on non-main ranks, matching the
local save_pretrained_gguf lora branch, so only rank 0 converts/uploads.
- compressed export VLM detection: require a vision_config or a
ForVisionText2Text architecture; a bare *ForConditionalGeneration also
matches text seq2seq models (T5/BART/Whisper) and is no longer treated as
a VLM on its own.
- GGUF GPU smoke test: drop SFTConfig(max_length=1024), which raises under
newer TRL padding-free training; length enforcement is not needed here.
* Add imatrix option to GGUF export, enabling IQ low-bit quants
save_pretrained_gguf / push_to_hub_gguf gain imatrix_file:
None -> no imatrix (unchanged)
'/path' -> pass to llama-quantize --imatrix (a *.gguf_file is renamed to *.gguf)
True -> download the upstream unsloth/<base>-GGUF imatrix (imatrix_unsloth.dat or
.gguf_file), raising a clear error if none exists
An importance matrix unlocks the IQ low-bit quants (iq2_xxs, iq4_xs, ...), which were hard
disabled before. They are gated: requesting one without an imatrix raises a clear error.
- _resolve_imatrix_file resolves path/True (PEFT base first, normalized via get_model_name,
derives unsloth/<base>-GGUF, copies out of the HF cache before renaming *.gguf_file).
- IMATRIX_QUANTS registry replaces the old commented-out IQ entries; save_to_gguf accepts a
resolved imatrix and threads it into the quantize calls.
- The --imatrix flag is emitted by unsloth_zoo's quantize_gguf (companion change). save.py
fails fast with an upgrade hint if the installed unsloth_zoo lacks the imatrix kwarg.
Tests: tests/saving/test_imatrix_export.py (CPU: resolution, repo derivation, IQ gate,
--imatrix wiring) wired into CI; tests/saving/test_gguf_export_and_inference.py extended with
GPU iq2_xxs/iq4_xs export + inference. Verified end to end on Llama-3.2-1B: imatrix
auto-downloaded, iq2_xxs/iq4_xs exported and run via llama.cpp.
Note: requires the companion unsloth_zoo quantize_gguf imatrix change.
* Address imatrix/compressed review feedback: unsloth org GGUF repo, fail-fast, calibration split
- imatrix auto-resolve (imatrix_file=True): derive the upstream repo as unsloth/<base>-GGUF
instead of <org>/<base>-GGUF, so official bases (e.g. meta-llama/Llama-3.1-8B-Instruct) find
the matching Unsloth GGUF imatrix repo rather than failing on a nonexistent meta-llama/...-GGUF.
- Resolve/validate the imatrix before the 16-bit merge in save_pretrained_gguf, so a bad path or
an unavailable upstream imatrix fails fast instead of after a long, multi-GB merge.
- Compressed calibration: when a Hub dataset has no "train" split, resolve the first split name
and slice it, instead of materializing the whole dataset just to take num_samples rows. Keeps
the original materialize-then-subselect path as a last resort.
Tests: add unsloth/<base>-GGUF mapping for an official base id, and create the imatrix file in the
quantize_gguf flag test (quantize_gguf now validates the imatrix exists).
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
347 lines
14 KiB
Python
347 lines
14 KiB
Python
# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Standalone llm-compressor runner for Unsloth's FP8/FP4 export.
|
|
|
|
Launched as a subprocess by file path (not `python -m`) so the Unsloth package, which patches
|
|
transformers attention, is not imported here; llm-compressor needs an unpatched forward for
|
|
calibration (e.g. NVFP4). Reads a merged 16bit checkpoint, writes a compressed-tensors one.
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
|
|
def _is_moe(config):
|
|
"""True if the model config looks like a sparse Mixture-of-Experts model."""
|
|
if config is None:
|
|
return False
|
|
for cfg in (config, getattr(config, "text_config", None)):
|
|
if cfg is None:
|
|
continue
|
|
for attr in ("num_experts", "num_local_experts", "n_routed_experts", "moe_num_experts"):
|
|
v = getattr(cfg, attr, None)
|
|
if isinstance(v, int) and v > 1:
|
|
return True
|
|
return "moe" in (getattr(config, "model_type", "") or "").lower()
|
|
|
|
|
|
def _has_mtp(config):
|
|
"""True if the model carries MTP / speculative-decoding layers (e.g. Qwen3-Next, DeepSeek)."""
|
|
if config is None:
|
|
return False
|
|
mt = (getattr(config, "model_type", "") or "").lower()
|
|
if "qwen3_next" in mt or "mtp" in mt:
|
|
return True
|
|
for attr in ("num_nextn_predict_layers", "num_mtp_layers", "mtp_num_layers"):
|
|
v = getattr(config, attr, None)
|
|
if isinstance(v, int) and v > 0:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _build_calibration_dataset(tokenizer, kind, value, num_samples, max_seq_length):
|
|
from datasets import DatasetDict, load_dataset, load_from_disk
|
|
|
|
_tok = tokenizer.tokenizer if hasattr(tokenizer, "tokenizer") else tokenizer
|
|
|
|
if kind == "none":
|
|
print(
|
|
f"Unsloth: NVFP4 needs calibration data. Defaulting to {num_samples} samples of "
|
|
"HuggingFaceH4/ultrachat_200k. For best accuracy pass your own training data via "
|
|
"`calibration_dataset=...`.",
|
|
flush = True,
|
|
)
|
|
ds = load_dataset("HuggingFaceH4/ultrachat_200k", split = f"train_sft[:{num_samples}]")
|
|
ds = ds.shuffle(seed = 42)
|
|
elif kind == "hfid":
|
|
# Not every dataset has a "train" split (e.g. train_sft only); fall back to the first one.
|
|
try:
|
|
ds = load_dataset(value, split = f"train[:{num_samples}]")
|
|
except (ValueError, KeyError):
|
|
from datasets import get_dataset_split_names
|
|
try:
|
|
# Resolve the first split name so only num_samples rows are fetched, instead of
|
|
# downloading/materializing the whole dataset just to take a small slice.
|
|
split = get_dataset_split_names(value)[0]
|
|
ds = load_dataset(value, split = f"{split}[:{num_samples}]")
|
|
except Exception:
|
|
# Last resort: materialize, then subselect (preserves the original behavior).
|
|
ds = load_dataset(value)
|
|
if isinstance(ds, DatasetDict):
|
|
ds = ds[next(iter(ds.keys()))]
|
|
if num_samples and len(ds) > num_samples:
|
|
ds = ds.select(range(num_samples))
|
|
ds = ds.shuffle(seed = 42)
|
|
elif kind == "disk":
|
|
ds = load_from_disk(value)
|
|
if isinstance(ds, DatasetDict):
|
|
if "train" in ds:
|
|
ds = ds["train"]
|
|
elif len(ds) == 1:
|
|
ds = next(iter(ds.values()))
|
|
else:
|
|
raise RuntimeError(
|
|
"Unsloth: disk calibration_dataset is a DatasetDict with multiple splits; "
|
|
"pass a single split, e.g. calibration_dataset=dataset['train']."
|
|
)
|
|
if num_samples and len(ds) > num_samples:
|
|
ds = ds.shuffle(seed = 42).select(range(num_samples))
|
|
else:
|
|
raise ValueError(f"Unknown calibration-dataset-kind: {kind}")
|
|
|
|
try:
|
|
if len(ds) == 0:
|
|
raise RuntimeError(
|
|
"Unsloth: the calibration dataset is empty after loading/subsampling; "
|
|
"pass a non-empty calibration_dataset."
|
|
)
|
|
except TypeError:
|
|
pass # streaming / iterable datasets have no len(); let llm-compressor handle them
|
|
|
|
cols = set(ds.column_names)
|
|
if "input_ids" in cols:
|
|
# Drop non-model-input columns (e.g. a leftover 'messages' list) so llm-compressor's
|
|
# collator does not try to batch them.
|
|
keep = {"input_ids", "attention_mask", "labels", "position_ids"}
|
|
extra = [c for c in ds.column_names if c not in keep]
|
|
if extra:
|
|
ds = ds.remove_columns(extra)
|
|
return ds
|
|
if "messages" in cols:
|
|
# Base / non-chat tokenizers have no chat template; concatenate message contents instead
|
|
# of calling apply_chat_template (which would raise).
|
|
has_chat_template = bool(getattr(_tok, "chat_template", None))
|
|
|
|
def _content_to_text(content):
|
|
# content may be a str, None, or a multimodal list of parts (str or {"text": ...}).
|
|
if content is None:
|
|
return ""
|
|
if isinstance(content, str):
|
|
return content
|
|
if isinstance(content, (list, tuple)):
|
|
parts = []
|
|
for part in content:
|
|
if isinstance(part, str):
|
|
parts.append(part)
|
|
elif isinstance(part, dict):
|
|
text = part.get("text") or part.get("content")
|
|
if isinstance(text, str):
|
|
parts.append(text)
|
|
return " ".join(parts)
|
|
return str(content)
|
|
|
|
def _prep(ex):
|
|
msgs = ex["messages"] or []
|
|
if has_chat_template:
|
|
return {"text": _tok.apply_chat_template(msgs, tokenize = False)}
|
|
return {"text": "\n".join(_content_to_text(m.get("content")) for m in msgs)}
|
|
|
|
ds = ds.map(_prep)
|
|
elif "text" not in cols:
|
|
raise RuntimeError(
|
|
"Unsloth: calibration_dataset must contain a 'messages', 'text', or 'input_ids' "
|
|
f"column (got: {sorted(cols)})."
|
|
)
|
|
|
|
def _tokenize(sample):
|
|
return _tok(
|
|
sample["text"],
|
|
padding = False,
|
|
max_length = max_seq_length,
|
|
truncation = True,
|
|
add_special_tokens = False,
|
|
)
|
|
|
|
return ds.map(_tokenize, remove_columns = ds.column_names)
|
|
|
|
|
|
def _from_pretrained(auto_model, model_path, trust_remote_code):
|
|
import torch
|
|
|
|
# transformers renamed torch_dtype -> dtype; support both.
|
|
try:
|
|
return auto_model.from_pretrained(
|
|
model_path,
|
|
device_map = "auto",
|
|
low_cpu_mem_usage = True,
|
|
trust_remote_code = trust_remote_code,
|
|
dtype = torch.bfloat16,
|
|
)
|
|
except TypeError:
|
|
return auto_model.from_pretrained(
|
|
model_path,
|
|
device_map = "auto",
|
|
low_cpu_mem_usage = True,
|
|
trust_remote_code = trust_remote_code,
|
|
torch_dtype = torch.bfloat16,
|
|
)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--model", required = True, help = "merged 16bit HF checkpoint dir")
|
|
ap.add_argument("--scheme", required = True)
|
|
ap.add_argument("--out", required = True)
|
|
ap.add_argument("--needs-calibration", action = "store_true")
|
|
ap.add_argument("--calibration-dataset-kind", default = "none", choices = ["none", "hfid", "disk"])
|
|
ap.add_argument("--calibration-dataset", default = "")
|
|
ap.add_argument("--num-calibration-samples", type = int, default = 512)
|
|
ap.add_argument("--max-seq-length", type = int, default = 2048)
|
|
ap.add_argument("--is-vlm", action = "store_true")
|
|
ap.add_argument("--trust-remote-code", action = "store_true")
|
|
ap.add_argument("--variant", default = "", help = "weight-filename variant for the output shards")
|
|
args = ap.parse_args()
|
|
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
from llmcompressor import oneshot
|
|
from llmcompressor.modifiers.quantization import QuantizationModifier
|
|
|
|
# Import the VLM auto-class only when needed - some transformers versions lack it, and the
|
|
# text path must not fail just because that newer class is unavailable.
|
|
if args.is_vlm:
|
|
from transformers import AutoProcessor
|
|
try:
|
|
from transformers import AutoModelForImageTextToText as _VLMModel
|
|
except ImportError:
|
|
try:
|
|
from transformers import AutoModelForVision2Seq as _VLMModel
|
|
except ImportError as e:
|
|
raise RuntimeError(
|
|
"Unsloth: this transformers version has no VLM auto-model class for "
|
|
"compressed multimodal export. Please upgrade transformers."
|
|
) from e
|
|
auto_model, auto_proc = _VLMModel, AutoProcessor
|
|
else:
|
|
auto_model, auto_proc = AutoModelForCausalLM, AutoTokenizer
|
|
|
|
model = _from_pretrained(auto_model, args.model, args.trust_remote_code)
|
|
model.eval()
|
|
# A tokenizer may be absent if the caller saved it separately; only calibration needs one.
|
|
try:
|
|
tokenizer = auto_proc.from_pretrained(args.model, trust_remote_code = args.trust_remote_code)
|
|
except Exception:
|
|
if args.needs_calibration:
|
|
raise RuntimeError(
|
|
f"Unsloth: calibration export needs a tokenizer but none was found in {args.model}. "
|
|
"Pass tokenizer=... to save_pretrained_merged."
|
|
)
|
|
tokenizer = None
|
|
|
|
# MoE models: keep the router/gate unquantized (it decides expert routing) and calibrate every
|
|
# expert even if the sample set does not route tokens to all of them.
|
|
is_moe = _is_moe(getattr(model, "config", None))
|
|
ignore = ["lm_head"]
|
|
if is_moe:
|
|
# Keep MoE routing layers unquantized: the router gate and (Qwen) shared-expert gate.
|
|
ignore += ["re:.*\\.gate$", "re:.*\\.shared_expert_gate$"]
|
|
moe_kwargs = {"moe_calibrate_all_experts": True} if is_moe else {}
|
|
|
|
def _make_recipe():
|
|
return QuantizationModifier(targets = "Linear", scheme = args.scheme, ignore = ignore)
|
|
|
|
if args.needs_calibration:
|
|
ds = _build_calibration_dataset(
|
|
tokenizer,
|
|
args.calibration_dataset_kind,
|
|
args.calibration_dataset,
|
|
args.num_calibration_samples,
|
|
args.max_seq_length,
|
|
)
|
|
# Use the sequential pipeline: it onloads layer-by-layer, so models that do not fit in
|
|
# memory at once can still calibrate. Running here in a clean process (Unsloth's attention
|
|
# patches are absent) means tracing works; fall back to the memory-hungry "basic" pipeline
|
|
# only if tracing fails.
|
|
try:
|
|
oneshot(
|
|
model = model,
|
|
dataset = ds,
|
|
recipe = _make_recipe(),
|
|
max_seq_length = args.max_seq_length,
|
|
num_calibration_samples = args.num_calibration_samples,
|
|
pipeline = "sequential",
|
|
**moe_kwargs,
|
|
)
|
|
except Exception as e:
|
|
print(
|
|
f"Unsloth: sequential calibration pipeline failed ({type(e).__name__}: {e}); "
|
|
"retrying with the 'basic' pipeline (needs the full model to fit in memory).",
|
|
flush = True,
|
|
)
|
|
# Free the partially-processed model before loading a fresh copy, so the fallback does
|
|
# not transiently hold two copies on GPU. llm-compressor keeps the model in a global
|
|
# session after a failed run, so reset it first; also drop the traceback frames (e) and
|
|
# the local reference that pin the model.
|
|
import gc as _gc
|
|
import torch as _torch
|
|
|
|
try:
|
|
from llmcompressor.core import reset_session
|
|
reset_session()
|
|
except Exception:
|
|
pass
|
|
e = None
|
|
del model
|
|
_gc.collect()
|
|
if _torch.cuda.is_available():
|
|
_torch.cuda.empty_cache()
|
|
model = _from_pretrained(auto_model, args.model, args.trust_remote_code)
|
|
model.eval()
|
|
oneshot(
|
|
model = model,
|
|
dataset = ds,
|
|
recipe = _make_recipe(),
|
|
max_seq_length = args.max_seq_length,
|
|
num_calibration_samples = args.num_calibration_samples,
|
|
pipeline = "basic",
|
|
**moe_kwargs,
|
|
)
|
|
else:
|
|
oneshot(model = model, recipe = _make_recipe())
|
|
|
|
os.makedirs(args.out, exist_ok = True)
|
|
save_kwargs = {"variant": args.variant} if args.variant else {}
|
|
model.save_pretrained(args.out, save_compressed = True, **save_kwargs)
|
|
if tokenizer is not None:
|
|
tokenizer.save_pretrained(args.out)
|
|
|
|
if _has_mtp(getattr(model, "config", None)):
|
|
print(
|
|
"Unsloth: WARNING - this model has MTP / speculative-decoding tensors that are not "
|
|
"included in the compressed export (only the main model is quantized and saved). Use "
|
|
"the non-compressed save path if you need the MTP weights.",
|
|
flush = True,
|
|
)
|
|
|
|
cfg_path = os.path.join(args.out, "config.json")
|
|
cfg = {}
|
|
if os.path.exists(cfg_path):
|
|
with open(cfg_path, "r", encoding = "utf-8") as f:
|
|
cfg = json.load(f)
|
|
if "quantization_config" not in cfg:
|
|
print(f"Unsloth: ERROR - no quantization_config written to {cfg_path}", flush = True)
|
|
sys.exit(2)
|
|
shards = glob.glob(os.path.join(args.out, "*.safetensors"))
|
|
qfmt = cfg["quantization_config"].get("format")
|
|
print(
|
|
f"[compressed-quantize] OK scheme={args.scheme} format={qfmt} "
|
|
f"shards={len(shards)} -> {args.out}",
|
|
flush = True,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|