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
This commit is contained in:
parent
060425d1a8
commit
03be1d30f4
2 changed files with 41 additions and 8 deletions
|
|
@ -71,9 +71,14 @@ def _build_calibration_dataset(tokenizer, kind, value, num_samples, max_seq_leng
|
|||
if "input_ids" in cols:
|
||||
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 _prep(ex):
|
||||
return {"text": _tok.apply_chat_template(ex["messages"], tokenize = False)}
|
||||
if has_chat_template:
|
||||
return {"text": _tok.apply_chat_template(ex["messages"], tokenize = False)}
|
||||
return {"text": "\n".join(m.get("content", "") for m in ex["messages"])}
|
||||
|
||||
ds = ds.map(_prep)
|
||||
elif "text" not in cols:
|
||||
|
|
@ -176,10 +181,10 @@ def main():
|
|||
args.num_calibration_samples,
|
||||
args.max_seq_length,
|
||||
)
|
||||
# Let llm-compressor pick its default (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.
|
||||
# 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,
|
||||
|
|
@ -187,6 +192,7 @@ def main():
|
|||
recipe = _make_recipe(),
|
||||
max_seq_length = args.max_seq_length,
|
||||
num_calibration_samples = args.num_calibration_samples,
|
||||
pipeline = "sequential",
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
|
|
|
|||
|
|
@ -3847,7 +3847,7 @@ def _unsloth_save_compressed_tensors(
|
|||
|
||||
# 2) Pick the local working dir. For a hub push, save_directory is a repo id, so merge and
|
||||
# quantize inside an isolated temp dir instead of writing ./<repo_id> into the cwd.
|
||||
repo_id, work_tmp, calib_tmp = None, None, None
|
||||
repo_id, work_tmp, calib_tmp, model_dev = None, None, None, None
|
||||
if push_to_hub:
|
||||
repo_id = os.fspath(save_directory)
|
||||
work_tmp = tempfile.mkdtemp(prefix = "unsloth-compressed-")
|
||||
|
|
@ -3929,8 +3929,7 @@ def _unsloth_save_compressed_tensors(
|
|||
)
|
||||
except Exception:
|
||||
ds_to_save = calibration_dataset
|
||||
parent = os.path.dirname(os.path.abspath(local_dir)) or None
|
||||
calib_tmp = tempfile.mkdtemp(prefix = "unsloth-calib-", dir = parent)
|
||||
calib_tmp = tempfile.mkdtemp(prefix = "unsloth-calib-")
|
||||
shutil.rmtree(calib_tmp, ignore_errors = True) # save_to_disk wants a fresh path
|
||||
ds_to_save.save_to_disk(calib_tmp)
|
||||
calib_kind, calib_value = "disk", calib_tmp
|
||||
|
|
@ -3975,6 +3974,29 @@ def _unsloth_save_compressed_tensors(
|
|||
if trust_remote_code:
|
||||
cmd.append("--trust-remote-code")
|
||||
|
||||
# Free the in-memory model's CUDA memory before the subprocess loads its own copy from
|
||||
# disk, so a single GPU need not hold both at once. Best-effort and restored in finally;
|
||||
# skipped for quantized or multi-device models where moving is unsafe.
|
||||
try:
|
||||
if (
|
||||
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)
|
||||
):
|
||||
_devs = {str(p.device) for p in model.parameters()}
|
||||
if len(_devs) == 1 and next(iter(_devs)).startswith("cuda"):
|
||||
_dev = next(model.parameters()).device
|
||||
model.to("cpu")
|
||||
model_dev = _dev # set only after a successful move, so finally can restore
|
||||
except Exception:
|
||||
model_dev = None
|
||||
for _ in range(3):
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
print(
|
||||
f"Unsloth: Quantizing the merged model to {scheme} with llm-compressor "
|
||||
"(in a separate process)..."
|
||||
|
|
@ -4017,6 +4039,11 @@ def _unsloth_save_compressed_tensors(
|
|||
_print_compressed_hw_note(scheme, result)
|
||||
return result
|
||||
finally:
|
||||
if model_dev is not None:
|
||||
try:
|
||||
model.to(model_dev) # restore the model to its original device
|
||||
except Exception:
|
||||
pass
|
||||
if calib_tmp is not None and os.path.isdir(calib_tmp):
|
||||
shutil.rmtree(calib_tmp, ignore_errors = True)
|
||||
if work_tmp is not None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue