unsloth/scripts/clef_train_bench.py
Daniel Han 4451666e09
Train any text or vision LLM as a Clef decision model from Studio, with FastDecisionModel.predict and adapter saves (#12876)
* Studio: fine-tune Laya decision models and serve them

* Check decision checkpoints at their subfolder, refuse DoRA/LoftQ, steady the toy test

* Turn gradient checkpointing back on in FastDecisionModel.for_training

* Merge decision LoRA on a CPU copy, check decision subfolders before the repo root

* Log decision runs to TensorBoard when it is enabled

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Block a decision run whose model config failed to load, gate the GPU memory test on a real CUDA device

* Use the AGPL-3.0 SPDX header on the new decision model files

* Record the vendored laya import in the risky loader baseline

* Train Cloudflare Clef decision models with FastDecisionModel

* Install the Qwen3.5 fast-path kernels for Clef decision runs, test they engage

* Studio: train and serve Cloudflare Clef decision models

Clef and Clef-flash repos (config.json plus joint_head.*) are detected as
decision models next to Laya. Training goes through FastDecisionModel with
QLoRA allowed and the Qwen3.5 fast-path kernel setup the worker skips for
Laya. The Decision API lists clef and clef-flash and Clef fine-tunes, and
serves them from a spawn child that loads the backbone through Unsloth, so
unloading returns the GPU memory. Clef waits for the GPU while a run trains
instead of loading on CPU.

* Keep Clef off fp16 autocast and fp16 weights

* Vendor Cloudflare's Clef joint_schema_model.py unmodified

* Drop the decision trainer's backward rescale: accelerate never scales the loss here

* Run the decision toy task on CPU, where its dropout stream is fixed

* Fold Clef calibration into the saved head; add label smoothing and Brier loss options

* Build Clef's encoding and head on the vendored joint_schema_model.py

* Check Clef reloads against the folded temperature; skip Clef forwards on CPU when causal_conv1d is installed

* Batch the Clef head, pool its fp32 norm in chunks, checkpoint and compile it

* Record QLoRA in Clef run metadata, stub decision_layout in the export route test

* Decision runs: hold out from training when eval files are training files, honour a cancel during calibration

* Serve Clef with the trainer's temperature rule, incl. option-count buckets

* Fetch a decision checkpoint by its layout so Unsloth's download check accepts it

* Save decision models crash-safely and fold Clef's calibrated temperature into its head

* Load Clef in Unsloth's dynamic 4-bit and train it in float16 and bfloat16

* Add Cloudflare's Clef recipe options and record accuracy, drop the no-op accumulation wrapper

* Run the toy learning test on CPU, where its step budget was tuned

* Format the decision model changes

* Run the Clef head under torch's checkpoint and autocast's matmul dtype; add a train bench

* Refuse Clef on machines without an NVIDIA or AMD GPU, and mark it unavailable in Settings

* Read Clef availability in Settings from finished device detection only

* Keep a Clef save's tokenizer files and config version as the source has them

* Score the Clef bench's head parity against an fp32 per-record oracle

* State the AGPL header and Cloudflare derivation notice on clef.py

* Keep the Clef head's norm op off Unsloth's patched layer_norm; compile test after a model load

* Keep Clef's logit scales float32 on save and fit its temperature with a line search

* Skip the bfloat16 Clef training test on GPUs without bfloat16

* Read the Qwen3.5 kernel a layer picked on transformers before 5.6 too

* Turn a plain language model into a Clef-style decision model with FastDecisionModel

FastDecisionModel.from_pretrained(<causal LM or Qwen3.5 VLM>, decision_head = "clef") loads the
backbone through FastModel and adds a fresh joint schema head sized to its hidden states (width
1024 from hidden 3072 up, else 512; head_width / head_config / head_init override). The model
trains, evaluates, calibrates and saves like a Clef checkpoint. freeze_backbone / unfreeze_backbone
give the head-only warm-up stage.

* Convert public datasets into decision rows and mix them with schema augmentation

decision_datasets.py maps BANKING77, CLINC150 (with out of scope), MNLI, SNLI, WANLI, BoolQ,
AG News, SST-5, MMLU, CommonsenseQA, ARC, xLAM tool selection, deepset prompt injections and
LocalLLaMA/typed-decisions to {state, questions, gold} rows with their licences. augment_row
renames question and option ids, paraphrases or drops instructions, subsamples large option
sets around the gold, adds a derived question of another type, renders states as text and
shuffles field order. build_decision_mixture balances sources, refuses Decision Index
eval-only sets and drops rows sharing a 13-word shingle with given evaluation texts.

* Add a script that trains a decision model from a plain language model

scripts/train_decision_from_lm.py builds the public-dataset mixture (decontaminated against
the evaluation sets) plus LocalLLaMA/typed-decisions, adds a fresh Clef head, optionally warms
it up with the backbone frozen, trains LoRA plus the head, calibrates, and reports accuracy,
ECE and record accuracy on typed-decisions test, BANKING77 and CLINC150.

* Load plain language models with the Clef loading rules and ship Cloudflare's loader on save

A model converted with decision_head = "clef" now gets the same dynamic 4-bit config and
float32-path bookkeeping as a Clef checkpoint, records the source vocabulary for the save, and
copies the vendored unsloth/_vendor/clef/joint_schema_model.py into a Qwen3.5 save so
Cloudflare's load_release_model loads it. The example script reports FastDecisionModel.evaluate
metrics, and the test checks served probabilities through the folded head temperature.

* Compare reloaded decision logits within bf16 rounding of their scale

* Add the license header to the decision-from-LM tests

* Apply the gradient accumulation fix for Laya training on transformers 5.0 to 5.5

* Review the vendored Clef config read and skip the Clef head compile test on CPU

* Name the Clef norm-pool fake kernels instead of reusing _

* Add FastDecisionModel.predict and adapter plus head saves for Clef decision models

predict() answers one Decision API request for Laya, Clef and language models
converted with decision_head="clef", at the calibrated temperatures, in the
Decision API answer shape. save_pretrained on a LoRA Clef model now writes only
the adapters, the joint head and the configs; from_pretrained puts them back on
the base model. save_pretrained_merged stays the Cloudflare-compatible layout.

Ported from unslothai/unsloth#12796 onto the Clef joint schema head.

* Studio: train text and vision LLMs as Clef decision models and serve them

A decision run on a model that is not a Laya or Clef checkpoint now trains the
LLM with a fresh Clef joint schema head (decision_layout "llm"), with its own
recipe and QLoRA. The model config route answers as_decision=true for text and
vision LLMs. The run saves the merged Clef layout, so the existing Clef runtime
serves it through the Decision API; that runtime now answers through the
library's Clef decide, and also serves adapter plus head folders on their base.

* Studio: add Train as Decision model for text and vision LLMs on the Train page

Ported from unslothai/unsloth#12796. The choice asks the backend for the model
as a decision model, applies the LLM decision recipe, keeps QLoRA and the
recipe's context length for Clef and LLM decision runs, and is hidden for Laya,
Clef, audio and embedding models.

* Serve Clef with the calibration autocast rule, calibrate a one-row holdout, and give Clef full fine-tunes the full fine-tuning rate

* Send Clef's recipe optimizer instead of forcing torch AdamW on every decision run

* Stop a Clef worker that times out loading or that training overtakes

* Count every Clef question in the held-out decision count

* Never reuse a Clef worker after a timeout or a dead pipe

* Detect a local decision model's layout in the selected subfolder

* Refuse KL regularization for a fully fine-tuned Clef at construction

* Plan decision runs on the one GPU they use and keep an env-pinned local Clef's layout

* Keep full Clef fine-tunes unquantized, save a resized Laya vocabulary, and stop flagging exact-fit Clef prompts as truncated

* Load plain language models as Clef decision models by default and run the decision notebook

FastDecisionModel.from_pretrained on a model that is neither a Laya nor a Clef
checkpoint now adds a Clef head without decision_head. predict answers carry
"answer" (the option, True or False for noul, the level for score) and every
option's probability. DecisionTrainer checkpoints a Clef model as adapters plus
its float32 head and resumes from them, a float16 language model keeps fp16,
and a test runs unslothai/notebooks#380's call sequence on a tiny Qwen3.5.

* Stand in for unsloth in the Clef truncation test on Studio's backend CI

* Load Clef backbones on one device when no device_map is given, and make decision tests portable to multi-GPU, Windows and Apple Silicon hosts

* Load a plain LM backbone for a decision model on one device when no device_map is given

* Keep download caches out of Clef saves, read long states in predict, compile the Clef head only for long runs

The merged save downloads the base shards with local_dir set to the save
folder, which left .cache/huggingface in it; the staging copy now drops it and
the save tests check the folder holds only model files. build_dataset reports
how many inputs were cut at max_seq_length, and predict() reads up to 16384
tokens like Studio's Decision API. DecisionTrainer compiles the Clef head only
when the run is long enough to pay for the compile; UNSLOTH_CLEF_COMPILE=1 or 0
forces it either way.

* Score Clef metric passes in the run's batch size and drop the parent run's training record when loading a Clef checkpoint

* Compile the Clef head from 5000 forward passes, its measured break-even on Qwen3.5-2B

* Refuse Clef LoRA options it cannot apply and stop a loading Clef worker as soon as training starts

* Train Clef under the bf16 autocast it evaluates in when no precision is given, and keep ACCELERATE_MIXED_PRECISION in step for transformers 4.x

* Bound the Clef reload check by the logit scale, which varies run to run, and give its asserts messages

* Test that a plain LM decision model with float32 norms trains without a precision flag

* Bound the plain LM reload check by its measured spread across runs

* Score the plain LM reload check under the autocast serving uses

* Run the Clef head eagerly unless UNSLOTH_CLEF_COMPILE=1

Compiling the head costs minutes in every process, warm Inductor cache included, and
does not speed up a real backbone. Qwen3.5-2B, batch 8, 25 steps, same card per row:

  card       eager s/step  compiled s/step  compile cost per process
  L4         2.94          2.99             about 17 to 18 min
  G4         0.481         0.488            about 7 min
  A100       0.785         0.779            about 17 to 18 min
  T4         10.30         10.42-10.46      about 17 to 18 min
  Kaggle T4  10.47-10.48   10.46-10.53      about 16 to 17 min

Only the tiny test model, where the head is most of the work, gets faster compiled, so
no run length makes the old 5000-forward threshold pay off. The head now runs eagerly
by default, UNSLOTH_CLEF_COMPILE=1 opts in and 0 still forces it off. The threshold and
DecisionTrainer.train override that set it are removed.

* Autocast Clef inference in fp16 on GPUs without bf16

A plain language model loaded in fp16 on a T4 keeps fp32 norms, so predict, calibration
and serving ran its fp16 Linears on fp32 activations without autocast and failed with
"expected mat1 and mat2 to have the same dtype" (Colab T4 and Kaggle 2x T4, tiny Qwen3).
Inference now autocasts as Clef trains: fp16 there, bf16 where supported, never on
Unsloth's float32 path for Qwen3.5. Both T4s pass the decision tests with this change.

* Refuse decision training on MLX before the Clef GPU check, so Apple Silicon users see the MLX limit

* Trim comments in the decision-from-LM code and tests

* Trim comments in the LLM decision model training code and tests

* Fix decision-from-LM loading options, schema augmentation edge cases and mixture sizes

- 4-bit plus full finetuning no longer installs a bitsandbytes config
- revision, local_files_only and subfolder reach the plain LM load (CPU too)
- a renamed question never overwrites another; one-level scores get no threshold
- canonical_row copies before subsampling; the mixture returns n_rows exactly
- the training script sizes sources after unavailable ones drop out, decontaminates
  typed-decisions, and runs on CPU-only torch

* Fix Clef full finetuning checkpoints, adapter base revisions, GPT-2 style backbones and the decision toggle rollback

- reopening adapters with full_finetuning = True is refused instead of training only the head
- the base revision a plain LM loaded from is saved with the adapters and used on reload
- full finetuning checkpoints keep the exact head and restore through load_best_model_at_end
- backbones without .model (GPT-2) resolve their decoder through base_model
- a failed as_decision config request turns Train as decision model back off

* Drop a stray assertion left by the merge in the from-LM tests

---------

Co-authored-by: Nilay Yadav <nilayyadav10@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Etherl <61019402+Etherll@users.noreply.github.com>
2026-10-07 05:52:14 -07:00

305 lines
14 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved.
"""Clef training step benchmark and head parity check, one JSON line on stdout.
python scripts/clef_train_bench.py --model Cloudflare/clef-flash --load-in-4bit --seq-len 8192
python scripts/clef_train_bench.py --model tiny --fast off --dtype fp16
--fast on|off sets UNSLOTH_CLEF_FAST (Unsloth's batched, chunked and checkpointed head, compiled
too under UNSLOTH_CLEF_COMPILE=1, vs the per-record head). Whatever the arm, the first batch's head is also run both ways on the same
hidden states, so `parity` compares the two heads' logits and gradients in this process.
"""
import argparse
import json
import os
import sys
import tempfile
import time
parser = argparse.ArgumentParser()
parser.add_argument("--model", default = "tiny", help = "tiny, a Clef repo id or folder")
parser.add_argument("--load-in-4bit", action = "store_true")
parser.add_argument("--fast", choices = ["on", "off"], default = "on")
parser.add_argument("--dtype", choices = ["bf16", "fp16"], default = "bf16")
parser.add_argument("--seq-len", type = int, default = 4096)
parser.add_argument("--batch-size", type = int, default = 1)
parser.add_argument("--questions", type = int, default = 8)
parser.add_argument("--steps", type = int, default = 6)
parser.add_argument("--warmup", type = int, default = 2)
parser.add_argument("--log-recompiles", action = "store_true")
args = parser.parse_args()
os.environ["UNSLOTH_CLEF_FAST"] = "1" if args.fast == "on" else "0"
import torch # noqa: E402
from unsloth import FastDecisionModel # noqa: E402
from unsloth.models import clef, decision # noqa: E402
if args.log_recompiles:
torch._logging.set_logs(recompiles = True, graph_breaks = True)
TINY = "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration"
TINY_HEAD = dict(hidden_size = 8, width = 32, routing_layers = 2, layers = 2, heads = 4, feedforward = 64)
def tiny_checkpoint() -> str:
from safetensors.torch import save_file
from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
folder = os.path.join(tempfile.mkdtemp(prefix = "clef_tiny_"), "clef")
torch.manual_seed(0)
backbone = Qwen3_5ForConditionalGeneration.from_pretrained(TINY, dtype = torch.bfloat16)
backbone.save_pretrained(folder)
AutoProcessor.from_pretrained(TINY).save_pretrained(folder)
head = clef.JointSchemaHead(
**{**TINY_HEAD, "hidden_size": backbone.config.text_config.hidden_size}
)
with torch.no_grad():
for parameter in head.parameters():
parameter.add_(torch.randn_like(parameter) * 0.1)
save_file(
{k: v.to(torch.bfloat16) for k, v in head.state_dict().items()},
f"{folder}/joint_head.safetensors",
)
with open(f"{folder}/joint_head_config.json", "w") as file:
json.dump(head.config, file)
return folder
def rows(tokenizer, count):
# A long state filled to about --seq-len tokens, and --questions questions of 2 to 6 options.
filler = "The customer reports intermittent checkout failures across several regions. "
per = len(tokenizer(filler, add_special_tokens = False).input_ids)
state = filler * max(1, (args.seq_len - 120 * args.questions - 200) // per)
out = []
for index in range(count):
questions, gold = {}, {}
for q in range(args.questions):
options = [f"option {o} for field {q}" for o in range(2 + (q + index) % 5)]
kind = ("choice", "score", "noul")[q % 3]
if kind == "noul":
questions[f"f{q}"] = {"type": "noul", "instructions": f"Is statement {q} true?"}
gold[f"f{q}"] = {"label": "true" if (q + index) % 2 else "false"}
elif kind == "score":
questions[f"f{q}"] = {
"type": "score",
"instructions": f"Rate {q}",
"criteria": options,
}
gold[f"f{q}"] = {"label": (q + index) % len(options)}
else:
questions[f"f{q}"] = {
"type": "choice",
"instructions": f"Pick {q}",
"criteria": {f"o{o}": text for o, text in enumerate(options)},
}
gold[f"f{q}"] = {"label": f"o{index % len(options)}"}
out.append({"state": f"{index}: {state}", "questions": questions, "gold": gold})
return out
def head_parity(model, batch, amp):
# Same hidden states into the batched and the per-record head. Each is scored against the
# per-record head with autocast off (fp32 head, the oracle): under bf16 autocast both heads
# round their matmuls, so the batched head passes when its error is at most 1.5x the
# per-record head's plus one bf16 rounding step (kernel_verify_workflow step 2).
backbone = model._backbone()
text_model = getattr(backbone.model, "language_model", backbone.model)
with torch.no_grad():
hidden = text_model(
input_ids = batch["input_ids"], attention_mask = batch["attention_mask"], use_cache = False
).last_hidden_state
embedding = backbone.get_output_embeddings().weight.detach()
head = model.head
os.environ["UNSLOTH_CLEF_FAST"] = "1"
def run(forward, autocast):
head.zero_grad()
h = hidden.detach().clone().requires_grad_(True)
with torch.autocast("cuda", dtype = amp or torch.float32, enabled = autocast):
out = forward(
h, batch["input_ids"], batch["attention_mask"], batch["records"], embedding
)
flat = [q for record in out for q in record]
padded = torch.full((len(flat), max(len(z) for z in flat)), -1e4, device = h.device)
for i, z in enumerate(flat):
padded[i, : len(z)] = z.float()
generator = torch.Generator(device = h.device).manual_seed(0)
grad = torch.randn(padded.shape, device = h.device, generator = generator)
(padded * grad * (padded > -1e3)).sum().backward()
grads = {
n: p.grad.detach().float().clone()
for n, p in head.named_parameters()
if p.grad is not None
}
return padded.detach(), h.grad.detach().float(), grads
oracle = run(head.forward_per_record, False)
ours = run(head.forward, amp is not None)
reference = run(head.forward_per_record, amp is not None)
os.environ["UNSLOTH_CLEF_FAST"] = "1" if args.fast == "on" else "0"
head.zero_grad()
valid = oracle[0] > -1e3
def errors(got):
return {
"logits_max_abs": (got[0] - oracle[0])[valid].abs().max().item(),
"hidden_grad_rel_l2": ((got[1] - oracle[1]).norm() / oracle[1].norm()).item(),
"param_grad_rel_l2": max(
((got[2][n] - oracle[2][n]).norm() / oracle[2][n].norm().clamp_min(1e-30)).item()
for n in oracle[2]
),
}
ours_error, reference_error = errors(ours), errors(reference)
step = 2**-8 if amp is not None else 2**-20
scale = oracle[0][valid].abs().max().item()
allowed = {
"logits_max_abs": 1.5 * reference_error["logits_max_abs"] + step * scale,
"hidden_grad_rel_l2": 1.5 * reference_error["hidden_grad_rel_l2"] + step,
"param_grad_rel_l2": 1.5 * reference_error["param_grad_rel_l2"] + step,
}
return {
"batched_vs_oracle": ours_error,
"per_record_vs_oracle": reference_error,
"argmax_equal": bool(torch.equal(ours[0].argmax(-1), reference[0].argmax(-1))),
"verdict": "PASS" if all(ours_error[k] <= allowed[k] for k in allowed) else "FAIL",
}
def head_cost(model, batch, amp):
# Head forward + backward alone on detached hidden states: time and memory above the inputs.
backbone = model._backbone()
text_model = getattr(backbone.model, "language_model", backbone.model)
with torch.no_grad():
hidden = text_model(
input_ids = batch["input_ids"], attention_mask = batch["attention_mask"], use_cache = False
).last_hidden_state
embedding = backbone.get_output_embeddings().weight.detach()
times = []
for i in range(args.warmup + 3):
torch.cuda.synchronize()
torch.cuda.reset_peak_memory_stats()
base = torch.cuda.memory_allocated()
start = time.perf_counter()
h = hidden.detach().requires_grad_(True)
with torch.autocast("cuda", dtype = amp or torch.float32, enabled = amp is not None):
out = model.head(
h, batch["input_ids"], batch["attention_mask"], batch["records"], embedding
)
logits = getattr(out, "flat_padded", None)
if logits is None:
logits = torch.cat([q.sum().reshape(1) for record in out for q in record])
logits.float().sum().backward()
torch.cuda.synchronize()
if i >= args.warmup:
times.append(time.perf_counter() - start)
peak = torch.cuda.max_memory_allocated() - base
model.head.zero_grad()
times.sort()
return {"head_ms": 1000 * times[len(times) // 2], "head_peak_gb": peak / 2**30}
def _graphs():
from torch._dynamo.utils import counters
return counters["stats"]["unique_graphs"]
def main():
model_name = tiny_checkpoint() if args.model == "tiny" else args.model
dtype = torch.float16 if args.dtype == "fp16" else torch.bfloat16
model, processor = FastDecisionModel.from_pretrained(
model_name, max_seq_length = args.seq_len, load_in_4bit = args.load_in_4bit, dtype = dtype
)
model = FastDecisionModel.get_peft_model(model, r = 16, lora_alpha = 16)
count = args.batch_size * (args.steps + args.warmup)
items, report = FastDecisionModel.build_dataset(
rows(processor.tokenizer, count), processor, model
)
if not items:
sys.exit(f"no items: {report}")
tokenizer = getattr(processor, "tokenizer", processor)
collate = decision.ClefDataCollator(tokenizer.pad_token_id)
device = next(model.parameters()).device
batches = []
for start in range(0, len(items), args.batch_size):
batch = collate(items[start : start + args.batch_size])
batches.append({k: v.to(device) if torch.is_tensor(v) else v for k, v in batch.items()})
model.train()
amp = torch.bfloat16 if dtype == torch.bfloat16 and torch.cuda.is_bf16_supported() else None
parity = head_parity(model, batches[0], amp)
cost = head_cost(model, batches[0], amp)
optimizer = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr = 1e-5)
times, losses, all_times, graphs_after_step = [], [], [], []
torch.cuda.reset_peak_memory_stats()
for index, batch in enumerate(batches[: args.steps + args.warmup]):
batch = dict(batch)
target = batch.pop("target")
torch.cuda.synchronize()
start = time.perf_counter()
with torch.autocast("cuda", dtype = amp or torch.float32, enabled = amp is not None):
logits, _ = model(**batch)
loss = decision._soft_cross_entropy(logits, target, batch["marker_mask"])
loss.backward()
optimizer.step()
optimizer.zero_grad(set_to_none = True)
torch.cuda.synchronize()
all_times.append(round(time.perf_counter() - start, 4))
graphs_after_step.append(_graphs())
if index >= args.warmup:
times.append(all_times[-1])
losses.append(round(loss.item(), 5))
times.sort()
step = times[len(times) // 2]
from torch._dynamo.utils import compile_times, counters
names, values = compile_times(repr = "csv", aggregate = True)
compiled_frames = dict(zip(names, values))
print(
"JOB_RESULT "
+ json.dumps(
{
"model": args.model,
"load_in_4bit": args.load_in_4bit,
"fast": args.fast,
"dtype": args.dtype,
"seq_len": args.seq_len,
"tokens_per_row": len(items[0]["input_ids"]),
"batch_size": args.batch_size,
"s_per_step": round(step, 4),
"s_per_step_spread": [round(times[0], 4), round(times[-1], 4)],
"peak_alloc_gb": round(torch.cuda.max_memory_allocated() / 2**30, 3),
"peak_reserved_gb": round(torch.cuda.max_memory_reserved() / 2**30, 3),
"head_ms": round(cost["head_ms"], 2),
"head_share": round(cost["head_ms"] / 1000 / step, 4),
"head_peak_gb": round(cost["head_peak_gb"], 3),
"losses": losses,
"step_times": all_times,
"graphs_after_step": graphs_after_step,
"graph_breaks": sum(counters["graph_break"].values()),
"graph_break_reasons": list(counters["graph_break"])[:10],
"unique_graphs": counters["stats"]["unique_graphs"],
"compile_frames": compiled_frames,
"parity": parity,
"levers": {
"batched_head": os.environ.get("UNSLOTH_CLEF_FAST") != "0",
"chunked_norm_pool": os.environ.get("UNSLOTH_CLEF_FAST") != "0",
"head_checkpoint": os.environ.get("UNSLOTH_CLEF_FAST") != "0"
and os.environ.get("UNSLOTH_CLEF_CHECKPOINT", "1") != "0",
"head_compile": os.environ.get("UNSLOTH_CLEF_FAST") != "0"
and clef._compile_supported(device)
and "function" in clef._COMPILED,
},
"gpu": torch.cuda.get_device_name(),
"torch": torch.__version__,
"unsloth_file": sys.modules["unsloth"].__file__,
}
),
flush = True,
)
if __name__ == "__main__":
main()