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>
This commit is contained in:
JoshuaL3000 2026-07-29 06:38:57 +08:00 committed by GitHub
commit e662af769b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 97 additions and 70 deletions

View file

@ -372,12 +372,10 @@ jobs:
tests/python/test_fast_language_model_text_only.py \ tests/python/test_fast_language_model_text_only.py \
tests/test_bad_mappings_redirect.py \ tests/test_bad_mappings_redirect.py \
tests/test_prefetch_snapshot_scope.py \ tests/test_prefetch_snapshot_scope.py \
tests/test_gemma_2b_mapper_key.py \ tests/test_gemma_2b_mapper_key.py
--deselect 'tests/utils/test_attention_masks.py::test_run_attention_flash_varlen_receives_window_and_softcap' # test_run_attention_flash_varlen_receives_window_and_softcap was deselected
# The deselected test monkeypatches flash_attn_varlen_func, which is # until attention_dispatch.py predefined flash_attn_varlen_func as None; it
# only bound on the module when `flash_attn` is importable. flash_attn # monkeypatches that name, so it no longer needs flash_attn on this runner.
# requires CUDA + dev toolchain, which the CPU-only ubuntu-latest
# runner does not have. The other Bucket-A tests pass cleanly.
- name: unsloth_zoo @ ${{ env.UNSLOTH_ZOO_REF }} — full pytest (CPU) - name: unsloth_zoo @ ${{ env.UNSLOTH_ZOO_REF }} — full pytest (CPU)
# 106 of 111 test_* in unsloth_zoo are CPU-only. The two CUDA-skip # 106 of 111 test_* in unsloth_zoo are CPU-only. The two CUDA-skip

View file

@ -96,12 +96,11 @@ def load_and_compute_8bit_ppl(
if __name__ == "__main__": if __name__ == "__main__":
mp.set_start_method("spawn", force = True) mp.set_start_method("spawn", force = True)
if torch.cuda.is_bf16_supported(): from unsloth import is_bfloat16_supported
compute_dtype = torch.bfloat16 from unsloth.models._utils import HAS_FLASH_ATTENTION
attn_implementation = "flash_attention_2"
else: compute_dtype = torch.bfloat16 if is_bfloat16_supported() else torch.float16
compute_dtype = torch.float16 attn_implementation = "flash_attention_2" if HAS_FLASH_ATTENTION else "sdpa"
attn_implementation = "sdpa"
model, tokenizer = FastLanguageModel.from_pretrained( model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/Llama-3.2-3B-Instruct", model_name = "unsloth/Llama-3.2-3B-Instruct",

View file

@ -121,12 +121,11 @@ def load_and_compute_8bit_ppl(
if __name__ == "__main__": if __name__ == "__main__":
mp.set_start_method("spawn", force = True) mp.set_start_method("spawn", force = True)
if torch.cuda.is_bf16_supported(): from unsloth import is_bfloat16_supported
compute_dtype = torch.bfloat16 from unsloth.models._utils import HAS_FLASH_ATTENTION
attn_implementation = "flash_attention_2"
else: compute_dtype = torch.bfloat16 if is_bfloat16_supported() else torch.float16
compute_dtype = torch.float16 attn_implementation = "flash_attention_2" if HAS_FLASH_ATTENTION else "sdpa"
attn_implementation = "sdpa"
model, tokenizer = FastLanguageModel.from_pretrained( model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/mistral-7b-v0.3", model_name = "unsloth/mistral-7b-v0.3",

View file

@ -98,12 +98,11 @@ def load_and_compute_8bit_ppl(
if __name__ == "__main__": if __name__ == "__main__":
mp.set_start_method("spawn", force = True) mp.set_start_method("spawn", force = True)
if torch.cuda.is_bf16_supported(): from unsloth import is_bfloat16_supported
compute_dtype = torch.bfloat16 from unsloth.models._utils import HAS_FLASH_ATTENTION
attn_implementation = "flash_attention_2"
else: compute_dtype = torch.bfloat16 if is_bfloat16_supported() else torch.float16
compute_dtype = torch.float16 attn_implementation = "flash_attention_2" if HAS_FLASH_ATTENTION else "sdpa"
attn_implementation = "sdpa"
model, tokenizer = FastLanguageModel.from_pretrained( model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/Phi-4", model_name = "unsloth/Phi-4",

View file

@ -95,12 +95,11 @@ def load_and_compute_8bit_ppl(
if __name__ == "__main__": if __name__ == "__main__":
mp.set_start_method("spawn", force = True) mp.set_start_method("spawn", force = True)
if torch.cuda.is_bf16_supported(): from unsloth import is_bfloat16_supported
compute_dtype = torch.bfloat16 from unsloth.models._utils import HAS_FLASH_ATTENTION
attn_implementation = "flash_attention_2"
else: compute_dtype = torch.bfloat16 if is_bfloat16_supported() else torch.float16
compute_dtype = torch.float16 attn_implementation = "flash_attention_2" if HAS_FLASH_ATTENTION else "sdpa"
attn_implementation = "sdpa"
model, tokenizer = FastLanguageModel.from_pretrained( model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/Llama-3.1-8B-Instruct", model_name = "unsloth/Llama-3.1-8B-Instruct",

View file

@ -164,12 +164,11 @@ def load_and_compute_8bit_ppl(
if __name__ == "__main__": if __name__ == "__main__":
mp.set_start_method("spawn", force = True) mp.set_start_method("spawn", force = True)
if torch.cuda.is_bf16_supported(): from unsloth import is_bfloat16_supported
compute_dtype = torch.bfloat16 from unsloth.models._utils import HAS_FLASH_ATTENTION
attn_implementation = "flash_attention_2"
else: compute_dtype = torch.bfloat16 if is_bfloat16_supported() else torch.float16
compute_dtype = torch.float16 attn_implementation = "flash_attention_2" if HAS_FLASH_ATTENTION else "sdpa"
attn_implementation = "sdpa"
model, tokenizer = FastLanguageModel.from_pretrained( model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/Qwen2.5-7B-Instruct", model_name = "unsloth/Qwen2.5-7B-Instruct",
@ -210,8 +209,6 @@ if __name__ == "__main__":
loftq_config = None, loftq_config = None,
) )
from unsloth import is_bfloat16_supported
trainer = SFTTrainer( trainer = SFTTrainer(
model = model, model = model,
tokenizer = tokenizer, tokenizer = tokenizer,

View file

@ -11,7 +11,11 @@ dequant reference.
import pytest import pytest
import torch import torch
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason = "needs CUDA") cuda_available = torch.cuda.is_available()
xpu_available = hasattr(torch, "xpu") and torch.xpu.is_available()
dev = "cuda" if cuda_available else "xpu" if xpu_available else "cpu"
pytestmark = pytest.mark.skipif(not (cuda_available or xpu_available), reason = "needs CUDA or XPU")
def _reference(X, weight, scale, block): def _reference(X, weight, scale, block):
@ -27,7 +31,6 @@ def test_tiny_non_tileable_forward_backward_matches_reference():
from unsloth.kernels.fp8 import FP8BlockQuantLinear from unsloth.kernels.fp8 import FP8BlockQuantLinear
torch.manual_seed(0) torch.manual_seed(0)
dev = "cuda"
block = [128, 128] block = [128, 128]
m, n = 8, 8 # non-tileable, in-dim % 128 != 0 m, n = 8, 8 # non-tileable, in-dim % 128 != 0
weight = torch.randn(m, n, device = dev, dtype = torch.bfloat16) # (out=m, in=n) weight = torch.randn(m, n, device = dev, dtype = torch.bfloat16) # (out=m, in=n)
@ -50,7 +53,6 @@ def test_e8m0_scale_is_upcast_and_runs():
if not hasattr(torch, "float8_e8m0fnu"): if not hasattr(torch, "float8_e8m0fnu"):
pytest.skip("torch build lacks float8_e8m0fnu") pytest.skip("torch build lacks float8_e8m0fnu")
dev = "cuda"
m, n = 8, 8 m, n = 8, 8
weight = torch.randn(m, n, device = dev, dtype = torch.bfloat16) weight = torch.randn(m, n, device = dev, dtype = torch.bfloat16)
scale = (torch.rand(1, 1, device = dev) + 1.0).to(torch.float8_e8m0fnu) scale = (torch.rand(1, 1, device = dev) + 1.0).to(torch.float8_e8m0fnu)
@ -70,7 +72,6 @@ def test_rectangular_block_dequant_matches_reference():
from unsloth.kernels.fp8 import _blockwise_weight_dequant_any_shape from unsloth.kernels.fp8 import _blockwise_weight_dequant_any_shape
torch.manual_seed(0) torch.manual_seed(0)
dev = "cuda"
block = [64, 128] block = [64, 128]
m, n = 64, 256 # evenly tiled: 64 % 64 == 0, 256 % 128 == 0 m, n = 64, 256 # evenly tiled: 64 % 64 == 0, 256 % 128 == 0
weight = torch.randn(m, n, device = dev, dtype = torch.bfloat16) weight = torch.randn(m, n, device = dev, dtype = torch.bfloat16)
@ -94,7 +95,6 @@ def test_e8m0_scale_preserves_non_default_block_size_attr():
pytest.skip("torch build lacks float8_e8m0fnu") pytest.skip("torch build lacks float8_e8m0fnu")
torch.manual_seed(0) torch.manual_seed(0)
dev = "cuda"
block = [64, 64] block = [64, 64]
# in-dim 96 is not divisible by block[1]=64 -> forward takes the torch dequant # in-dim 96 is not divisible by block[1]=64 -> forward takes the torch dequant
# fallback (no fp8 matmul kernel). Scale shape (2, 2) validates for [64, 64] but # fallback (no fp8 matmul kernel). Scale shape (2, 2) validates for [64, 64] but

View file

@ -2,6 +2,9 @@ from tqdm import tqdm
import torch import torch
import pandas as pd import pandas as pd
# DEVICE_TYPE_TORCH, not DEVICE_TYPE: the latter can be "hip"/"mlx", which .to() rejects.
from unsloth.device_type import DEVICE_TYPE_TORCH
model_comparison_results = {} model_comparison_results = {}
@ -17,7 +20,7 @@ def ppl_model(model, tokenizer, dataset):
for begin_loc in range(0, seq_len, stride): for begin_loc in range(0, seq_len, stride):
end_loc = min(begin_loc + max_length, seq_len) end_loc = min(begin_loc + max_length, seq_len)
trg_len = end_loc - prev_end_loc trg_len = end_loc - prev_end_loc
input_ids = encodings.input_ids[:, begin_loc:end_loc].to("cuda") input_ids = encodings.input_ids[:, begin_loc:end_loc].to(DEVICE_TYPE_TORCH)
target_ids = input_ids.clone() target_ids = input_ids.clone()
target_ids[:, :-trg_len] = -100 target_ids[:, :-trg_len] = -100
pad_token_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0 pad_token_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0

View file

@ -4,7 +4,7 @@ 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 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 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 benign batch-numerics tie-flips deep in the sequence) and must not be
gibberish. Skipped without CUDA. Run: `python -m pytest gibberish. Skipped without a GPU. Run: `python -m pytest
tests/utils/test_batched_leftpad_generation_gpu.py -v`. tests/utils/test_batched_leftpad_generation_gpu.py -v`.
""" """
@ -12,8 +12,19 @@ import pytest
import torch import torch
cuda_available = torch.cuda.is_available() 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"
pytestmark = pytest.mark.skipif(not cuda_available, reason = "requires a CUDA GPU") # 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" MODEL_NAME = "unsloth/Qwen2.5-0.5B-Instruct"
MAX_NEW_TOKENS = 32 MAX_NEW_TOKENS = 32
@ -53,7 +64,7 @@ def _chat(tokenizer, prompt):
def _generate(model, tokenizer, texts): def _generate(model, tokenizer, texts):
inputs = tokenizer(texts, return_tensors = "pt", padding = True, add_special_tokens = False).to( inputs = tokenizer(texts, return_tensors = "pt", padding = True, add_special_tokens = False).to(
"cuda" device
) )
with torch.inference_mode(): with torch.inference_mode():
out = model.generate( out = model.generate(

View file

@ -44,6 +44,8 @@ def _build_packed_training_setup(tmp_path, device):
dtype = torch.bfloat16 dtype = torch.bfloat16
else: else:
dtype = torch.float16 dtype = torch.float16
elif device.type == "xpu":
dtype = torch.bfloat16
try: try:
model, tokenizer = FastLanguageModel.from_pretrained( model, tokenizer = FastLanguageModel.from_pretrained(
@ -76,8 +78,8 @@ def _build_packed_training_setup(tmp_path, device):
max_length = 64, max_length = 64,
logging_steps = 1, logging_steps = 1,
max_steps = 1, max_steps = 1,
fp16 = device.type == "cuda" and not torch.cuda.is_bf16_supported(), fp16 = dtype == torch.float16,
bf16 = device.type == "cuda" and torch.cuda.is_bf16_supported(), bf16 = dtype == torch.bfloat16,
dataset_num_proc = 1, dataset_num_proc = 1,
output_dir = str(tmp_path), output_dir = str(tmp_path),
packing = True, packing = True,
@ -974,7 +976,12 @@ def test_enable_sample_packing():
def test_enable_sample_packing_trl_collator(tmp_path): def test_enable_sample_packing_trl_collator(tmp_path):
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.xpu.is_available():
device = torch.device("xpu")
else:
device = torch.device("cpu")
model, _, trainer, _ = _build_packed_training_setup(tmp_path, device) model, _, trainer, _ = _build_packed_training_setup(tmp_path, device)
enable_sample_packing(model, trainer) enable_sample_packing(model, trainer)
@ -1030,7 +1037,12 @@ def test_enable_padding_free_metadata():
def test_packing_sdpa(tmp_path): def test_packing_sdpa(tmp_path):
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.xpu.is_available():
device = torch.device("xpu")
else:
device = torch.device("cpu")
model, batch, trainer, llama_mod = _build_packed_training_setup(tmp_path, device) model, batch, trainer, llama_mod = _build_packed_training_setup(tmp_path, device)
assert "packed_seq_lengths" in batch assert "packed_seq_lengths" in batch

View file

@ -130,8 +130,14 @@ def _test_fake_quantizers_are_called(
# Weight fake quantizers must always be called. # Weight fake quantizers must always be called.
assert child.weight_fake_quantizer.count == 1 assert child.weight_fake_quantizer.count == 1
if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.xpu.is_available():
device = torch.device("xpu")
else:
pytest.skip("No GPU available")
for k, v in example_inputs.items(): for k, v in example_inputs.items():
example_inputs[k] = v.cuda() example_inputs[k] = v.to(device)
model.apply(_swap_fake_quantizers) model.apply(_swap_fake_quantizers)
model(**example_inputs) model(**example_inputs)
model.apply(_assert_fake_quantizers_are_called) model.apply(_assert_fake_quantizers_are_called)

View file

@ -15,18 +15,20 @@ import pytest
import torch import torch
def _has_real_cuda(): def _has_real_gpu():
try: for backend in ("cuda", "xpu"):
torch.zeros(1).to("cuda") try:
return True torch.zeros(1).to(backend)
except Exception: return True
return False except Exception:
pass
return False
HAS_REAL_CUDA = _has_real_cuda() HAS_REAL_GPU = _has_real_gpu()
requires_cuda = pytest.mark.skipif( requires_gpu = pytest.mark.skipif(
not HAS_REAL_CUDA, not HAS_REAL_GPU,
reason = "LlamaRotaryEmbedding builds per-device CUDA caches in __init__", reason = "LlamaRotaryEmbedding builds per-device caches in __init__ (needs CUDA or XPU)",
) )
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[2]
@ -360,7 +362,7 @@ def _cos_at_position(rot, position):
# --- Layer 3: CUDA behavioral guard (real instantiation needs a device) --- # --- Layer 3: CUDA behavioral guard (real instantiation needs a device) ---
@requires_cuda @requires_gpu
def test_constructor_applies_llama3_scaling(): def test_constructor_applies_llama3_scaling():
config = _make_config(LLAMA3_ROPE_SCALING) config = _make_config(LLAMA3_ROPE_SCALING)
rot = _unsloth_rotary(config) rot = _unsloth_rotary(config)
@ -371,7 +373,7 @@ def test_constructor_applies_llama3_scaling():
), "LlamaRotaryEmbedding built from a llama3 config produced unscaled inv_freq (issue #2405)." ), "LlamaRotaryEmbedding built from a llama3 config produced unscaled inv_freq (issue #2405)."
@requires_cuda @requires_gpu
def test_constructor_unscaled_config_uses_vanilla_inv_freq(): def test_constructor_unscaled_config_uses_vanilla_inv_freq():
rot = _unsloth_rotary(_make_config(None)) rot = _unsloth_rotary(_make_config(None))
got = rot.inv_freq.float().cpu() got = rot.inv_freq.float().cpu()
@ -381,7 +383,7 @@ def test_constructor_unscaled_config_uses_vanilla_inv_freq():
), "LlamaRotaryEmbedding with no rope_scaling must use the vanilla inv_freq" ), "LlamaRotaryEmbedding with no rope_scaling must use the vanilla inv_freq"
@requires_cuda @requires_gpu
def test_cos_cache_differs_between_scaled_and_unscaled_at_long_position(): def test_cos_cache_differs_between_scaled_and_unscaled_at_long_position():
scaled = _unsloth_rotary(_make_config(LLAMA3_ROPE_SCALING)) scaled = _unsloth_rotary(_make_config(LLAMA3_ROPE_SCALING))
unscaled = _unsloth_rotary(_make_config(None)) unscaled = _unsloth_rotary(_make_config(None))
@ -397,7 +399,7 @@ def test_cos_cache_differs_between_scaled_and_unscaled_at_long_position():
) )
@requires_cuda @requires_gpu
def test_extended_cache_keeps_scaling_after_growth(): def test_extended_cache_keeps_scaling_after_growth():
scaled = _unsloth_rotary(_make_config(LLAMA3_ROPE_SCALING)) scaled = _unsloth_rotary(_make_config(LLAMA3_ROPE_SCALING))
# Grow past the initial cache size (mirrors long-context decode). # Grow past the initial cache size (mirrors long-context decode).
@ -456,7 +458,7 @@ def _build_longrope_rotary():
return rot, config return rot, config
@requires_cuda @requires_gpu
@pytest.mark.parametrize( @pytest.mark.parametrize(
"build", [_build_llama3_rotary, _build_longrope_rotary], ids = ["llama3", "longrope"] "build", [_build_llama3_rotary, _build_longrope_rotary], ids = ["llama3", "longrope"]
) )

View file

@ -31,6 +31,8 @@ from ..utils.packing import (
build_xformers_block_causal_mask, build_xformers_block_causal_mask,
) )
flash_attn_func = None
flash_attn_varlen_func = None
if HAS_FLASH_ATTENTION: if HAS_FLASH_ATTENTION:
from flash_attn import flash_attn_func, flash_attn_varlen_func from flash_attn import flash_attn_func, flash_attn_varlen_func
HAS_XFORMERS = xformers is not None HAS_XFORMERS = xformers is not None