unsloth/tests/utils/test_batched_leftpad_generation_gpu.py
JoshuaL3000 e662af769b
fix: enable XPU support and update hardcoded CUDA selections for tests (#7401)
* fix: add XPU device support and update hardcoded CUDA selections

* fix: add XPU device support for pytest CUDA skipped tests

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

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

* Fix device handling for PR #7401

- perplexity_eval.py: use DEVICE_TYPE_TORCH, not DEVICE_TYPE. The latter can
  be "hip" or "mlx", which .to() rejects, so this regressed ROCm.
- test_batched_leftpad_generation_gpu.py: XPU diverges here today, so mark it
  non-strict xfail on XPU instead of reverting to a CUDA-only guard. Keeps the
  real XPU gap visible and turns green once it is fixed.
- Guard torch.xpu.is_available() with hasattr, matching device_type.py.

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

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

* Re-enable the flash varlen attention test in CI for PR #7401

attention_dispatch.py now predefines flash_attn_func / flash_attn_varlen_func
as None, so test_run_attention_flash_varlen_receives_window_and_softcap no
longer needs flash_attn importable to be monkeypatched. Verified on a runner
shaped like the CPU-only one: the test fails against main's attention_dispatch
and passes at this head, so the deselect is now dead weight.

* Tighten comments for PR #7401

Drop the hasattr rationale: torch.xpu has existed since torch 2.3 and the
dependency floor is 2.4, so no supported build predates the namespace. The
guard stays as cheap defence, but the comment claimed something untrue.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-07-28 15:38:57 -07:00

112 lines
3.9 KiB
Python

"""End-to-end GPU guard for batched left-padded generation (issues #1066, #3699).
Greedy generation in a left-padded batch must match solo batch-size-1
generation for the first PREFIX_TOKENS tokens (the bug makes padded rows
diverge into garbage immediately; a full-length match would be flaky due to
benign batch-numerics tie-flips deep in the sequence) and must not be
gibberish. Skipped without a GPU. Run: `python -m pytest
tests/utils/test_batched_leftpad_generation_gpu.py -v`.
"""
import pytest
import torch
cuda_available = torch.cuda.is_available()
xpu_available = hasattr(torch, "xpu") and torch.xpu.is_available()
device = "cuda" if cuda_available else "xpu" if xpu_available else "cpu"
# Non-strict rather than CUDA-only: keeps the XPU divergence visible, and goes
# green by itself once XPU generation is fixed.
pytestmark = [
pytest.mark.skipif(not (cuda_available or xpu_available), reason = "requires a CUDA or XPU GPU"),
pytest.mark.xfail(
xpu_available and not cuda_available,
reason = "batched left-padded generation diverges on XPU",
strict = False,
),
]
MODEL_NAME = "unsloth/Qwen2.5-0.5B-Instruct"
MAX_NEW_TOKENS = 32
PREFIX_TOKENS = 16
PROMPTS = [
"Give me a short introduction to large language model.",
"Here is an experiment log: "
+ " ".join(f"run {i} completed with stable throughput and no anomalies;" for i in range(1, 41))
+ " In one sentence, what is the overall conclusion?",
]
@pytest.fixture(scope = "module")
def model_and_tokenizer():
from unsloth import FastLanguageModel
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = MODEL_NAME,
max_seq_length = 2048,
load_in_4bit = True,
)
FastLanguageModel.for_inference(model)
tokenizer.padding_side = "left"
if tokenizer.pad_token_id is None:
tokenizer.pad_token_id = tokenizer.eos_token_id
return model, tokenizer
def _chat(tokenizer, prompt):
return tokenizer.apply_chat_template(
[{"role": "user", "content": prompt}],
tokenize = False,
add_generation_prompt = True,
)
def _generate(model, tokenizer, texts):
inputs = tokenizer(texts, return_tensors = "pt", padding = True, add_special_tokens = False).to(
device
)
with torch.inference_mode():
out = model.generate(
**inputs,
max_new_tokens = MAX_NEW_TOKENS,
do_sample = False,
temperature = None,
top_p = None,
top_k = None,
use_cache = True,
pad_token_id = tokenizer.pad_token_id,
)
suffixes = out[:, inputs["input_ids"].shape[1] :]
return [row.tolist() for row in suffixes]
def _looks_gibberish(text):
if not text.strip():
return True
exclam = text.count("!") / max(len(text), 1)
nonascii = sum(1 for c in text if ord(c) > 0x2FFF) / max(len(text), 1)
return exclam > 0.3 or nonascii > 0.5
def test_batched_leftpad_matches_solo_generation(model_and_tokenizer):
model, tokenizer = model_and_tokenizer
texts = [_chat(tokenizer, p) for p in PROMPTS]
solo = [_generate(model, tokenizer, [t])[0] for t in texts]
batched = _generate(model, tokenizer, texts)
for i, prompt in enumerate(PROMPTS):
solo_text = tokenizer.decode(solo[i], skip_special_tokens = True)
batch_text = tokenizer.decode(batched[i], skip_special_tokens = True)
assert batched[i][:PREFIX_TOKENS] == solo[i][:PREFIX_TOKENS], (
f"prompt {i} ({prompt[:30]!r}...) diverged from solo generation "
f"within the first {PREFIX_TOKENS} tokens inside a left-padded "
"batch; batched left-padded generation is broken again "
f"(issues #1066, #3699).\n"
f"solo : {solo_text!r}\nbatched: {batch_text!r}"
)
assert not _looks_gibberish(batch_text), (
f"prompt {i} produced gibberish in a left-padded batch "
f"(issues #1066, #3699): {batch_text!r}"
)