From 2bbe8e4610e95a8ac5910e362757e50c9e66eda4 Mon Sep 17 00:00:00 2001 From: lkk12014402 Date: Mon, 27 Jul 2026 13:56:17 +0000 Subject: [PATCH 1/2] add rotation ut for xpu. Signed-off-by: lkk12014402 --- test/test_xpu/transform/__init__.py | 13 ++ test/test_xpu/transform/test_hadamard.py | 92 +++++++++ test/test_xpu/transform/test_spinquant.py | 222 ++++++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 test/test_xpu/transform/__init__.py create mode 100644 test/test_xpu/transform/test_hadamard.py create mode 100644 test/test_xpu/transform/test_spinquant.py diff --git a/test/test_xpu/transform/__init__.py b/test/test_xpu/transform/__init__.py new file mode 100644 index 0000000000..14a4924419 --- /dev/null +++ b/test/test_xpu/transform/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Intel Corporation +# +# 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. diff --git a/test/test_xpu/transform/test_hadamard.py b/test/test_xpu/transform/test_hadamard.py new file mode 100644 index 0000000000..e85df52985 --- /dev/null +++ b/test/test_xpu/transform/test_hadamard.py @@ -0,0 +1,92 @@ +# Copyright (c) 2026 Intel Corporation +# +# 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. + +"""XPU CI tests for the Hadamard rotation transform. + +XPU counterpart of test/test_cuda/transform/test_mxfp4_transform.py. Covers: +- Direct ``apply_rotation`` (hadamard backend) on an XPU-resident model +- AutoRound pipeline with ``rotation_config="default"`` / ``"random_hadamard"`` + on XPU: quantize → save → load → generate +""" + +import shutil + +import pytest +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + +from auto_round import AutoRound +from auto_round.algorithms.transforms import apply_rotation, normalize_rotation_config + +from ...helpers import generate_prompt + +DEVICE = "xpu" + +pytestmark = pytest.mark.skipif( + not (hasattr(torch, "xpu") and torch.xpu.is_available()), + reason="XPU is not available on this host", +) + + +class TestHadamardApplyXPU: + """Direct apply_rotation on an XPU-resident model.""" + + def test_apply_rotation_forward(self, tiny_opt_model_path): + """Hadamard rotation applied on XPU should produce valid logits.""" + model = AutoModelForCausalLM.from_pretrained(tiny_opt_model_path, dtype=torch.float32).to(DEVICE).eval() + cfg = normalize_rotation_config("hadamard") + model = apply_rotation(model, cfg) + + tokenizer = AutoTokenizer.from_pretrained(tiny_opt_model_path) + inputs = tokenizer("The capital of France is", return_tensors="pt").to(DEVICE) + with torch.no_grad(): + logits = model(**inputs).logits + + assert logits.device.type == DEVICE + assert not torch.isnan(logits).any(), "NaN logits after hadamard rotation on XPU" + assert not torch.isinf(logits).any(), "Inf logits after hadamard rotation on XPU" + assert logits.abs().sum() > 0, "All-zero logits after hadamard rotation on XPU" + del model + torch.xpu.empty_cache() + + +class TestHadamardPipelineXPU: + """AutoRound pipeline with hadamard rotation on XPU (MXFP4 scheme).""" + + @pytest.fixture(autouse=True) + def _save_dir(self, tmp_path): + self.save_dir = str(tmp_path / "saved") + yield + shutil.rmtree(self.save_dir, ignore_errors=True) + + @pytest.mark.parametrize("rotation", ["default", "random_hadamard"]) + def test_transform_mxfp4_quant_infer(self, tiny_opt_model_path, rotation): + """MXFP4 + hadamard rotation: quantize → save → load on XPU → generate.""" + ar = AutoRound( + model=tiny_opt_model_path, + iters=0, + seqlen=8, + nsamples=2, + scheme="MXFP4", + rotation_config=rotation, + device_map=DEVICE, + ) + _, quantized_model_path = ar.quantize_and_save(output_dir=self.save_dir, format="auto_round") + + model = AutoModelForCausalLM.from_pretrained(quantized_model_path, dtype="auto", device_map=DEVICE) + tokenizer = AutoTokenizer.from_pretrained(quantized_model_path) + output = generate_prompt(model, tokenizer, device=DEVICE) + assert len(output) > 0, "Quantized model should produce non-empty output" + del model + torch.xpu.empty_cache() diff --git a/test/test_xpu/transform/test_spinquant.py b/test/test_xpu/transform/test_spinquant.py new file mode 100644 index 0000000000..d55da7a398 --- /dev/null +++ b/test/test_xpu/transform/test_spinquant.py @@ -0,0 +1,222 @@ +# Copyright (c) 2026 Intel Corporation +# +# 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. + +"""XPU CI tests for the SpinQuant/QuaRot rotation transform. + +XPU counterpart of test/test_cuda/transform/test_spinquant.py. Covers: +- Rotation correctness: R1, R1+R2, R1+R2+R3+R4 produce valid logits on XPU +- Rotation equivalence: rotation preserves model output (cosine similarity) +- Hook lifecycle: SpinQuant-tagged hooks are registered and selectively removed +- Pipeline integration via AutoRound(rotation_config="quarot") on XPU +""" + +import shutil + +import pytest +import torch +import torch.nn.functional as F +from transformers import AutoModelForCausalLM, AutoTokenizer + +from auto_round import AutoRound +from auto_round.algorithms.transforms import apply_rotation +from auto_round.algorithms.transforms.spinquant import SpinQuantConfig +from auto_round.algorithms.transforms.spinquant.preprocessor import remove_spinquant_hooks_from_model + +from ...helpers import generate_prompt + +DEVICE = "xpu" + +pytestmark = pytest.mark.skipif( + not (hasattr(torch, "xpu") and torch.xpu.is_available()), + reason="XPU is not available on this host", +) + +PROMPT = "The capital of France is" + + +def _load_model(model_path, dtype=torch.float32): + return AutoModelForCausalLM.from_pretrained(model_path, dtype=dtype).to(DEVICE).eval() + + +def _get_logits(model, tokenizer, text=PROMPT): + inputs = tokenizer(text, return_tensors="pt").to(model.device) + with torch.no_grad(): + return model(**inputs).logits.cpu() + + +class TestRotationCorrectnessXPU: + """Rotation configurations should produce valid (non-NaN, non-Inf) logits on XPU.""" + + @pytest.mark.parametrize( + "r1,r2,r3,r4,label", + [ + (True, False, False, False, "R1"), + (True, True, False, False, "R1+R2"), + (True, True, True, True, "R1+R2+R3+R4"), + ], + ) + def test_rotation_produces_valid_logits(self, tiny_qwen_model_path, r1, r2, r3, r4, label): + model = _load_model(tiny_qwen_model_path) + cfg = SpinQuantConfig( + r1=r1, + r2=r2, + r3=r3, + r4=r4, + online_r1_rotation=True, + trainable_rotation=False, + trainable_smooth=False, + ) + model = apply_rotation(model, cfg) + tokenizer = AutoTokenizer.from_pretrained(tiny_qwen_model_path) + logits = _get_logits(model, tokenizer) + + assert not torch.isnan(logits).any(), f"{label} rotation produced NaN logits" + assert not torch.isinf(logits).any(), f"{label} rotation produced Inf logits" + assert logits.abs().sum() > 0, f"{label} rotation produced all-zero logits" + del model + torch.xpu.empty_cache() + + +class TestRotationEquivalenceXPU: + """Rotation should preserve model output (functional equivalence) on XPU. + + R1 (online): activation hook + weight compensation → x·R·(R^T·W)^T = x·W^T + R2 (offline): head rotation fused into o_proj/next-layer weights + R3 (online): same rotation on Q and K after RoPE → (Q@R)(K@R)^T = Q@K^T + R4 (online + offline fuse): activation rotation + down_proj compensation + """ + + @pytest.mark.parametrize( + "r1,r2,r3,r4,label", + [ + (True, False, False, False, "R1"), + (False, True, False, False, "R2"), + (False, False, True, False, "R3"), + (False, False, False, True, "R4"), + (True, True, True, True, "R1+R2+R3+R4"), + ], + ) + def test_rotation_equivalence(self, tiny_qwen_model_path, r1, r2, r3, r4, label): + tokenizer = AutoTokenizer.from_pretrained(tiny_qwen_model_path) + + baseline_model = _load_model(tiny_qwen_model_path) + baseline_logits = _get_logits(baseline_model, tokenizer) + del baseline_model + torch.xpu.empty_cache() + + model = _load_model(tiny_qwen_model_path) + cfg = SpinQuantConfig( + r1=r1, + r2=r2, + r3=r3, + r4=r4, + online_r1_rotation=True, + trainable_rotation=False, + trainable_smooth=False, + ) + model = apply_rotation(model, cfg) + logits = _get_logits(model, tokenizer) + del model + torch.xpu.empty_cache() + + cos_sim = F.cosine_similarity( + baseline_logits.flatten().unsqueeze(0).float(), + logits.flatten().unsqueeze(0).float(), + ).item() + max_diff = (baseline_logits - logits).abs().max().item() + assert cos_sim > 0.9999, ( + f"{label} rotation broke model equivalence: cos_sim = {cos_sim:.6f}, max_diff = {max_diff:.4f}" + ) + + +class TestHookLifecycleXPU: + """SpinQuant hooks are properly tagged and selectively removed on XPU.""" + + def test_remove_only_spinquant_hooks(self, tiny_qwen_model_path): + model = _load_model(tiny_qwen_model_path, dtype=torch.float16) + + # Register a foreign hook + def foreign_hook(module, input): + return input + + first_linear = None + for m in model.modules(): + if isinstance(m, torch.nn.Linear): + first_linear = m + break + handle = first_linear.register_forward_pre_hook(foreign_hook) + + cfg = SpinQuantConfig( + r1=True, + r2=False, + r3=False, + r4=False, + online_r1_rotation=True, + trainable_rotation=False, + trainable_smooth=False, + ) + model = apply_rotation(model, cfg) + + # SpinQuant-tagged hooks should exist + tagged_hooks = 0 + for module in model.modules(): + for hook in module._forward_pre_hooks.values(): + if getattr(hook, "_spinquant_hook", False): + tagged_hooks += 1 + assert tagged_hooks > 0, "No SpinQuant-tagged hooks found" + + # Remove only spinquant hooks + remove_spinquant_hooks_from_model(model) + + # Foreign hook should still exist + assert handle.id in first_linear._forward_pre_hooks, "Foreign hook was incorrectly removed" + + # SpinQuant hooks should be gone + for module in model.modules(): + for hook in module._forward_pre_hooks.values(): + assert not getattr(hook, "_spinquant_hook", False), "SpinQuant hook was not removed" + + handle.remove() + del model + torch.xpu.empty_cache() + + +class TestPipelineIntegrationXPU: + """AutoRound(rotation_config='quarot') end-to-end on XPU.""" + + @pytest.fixture(autouse=True) + def _save_dir(self, tmp_path): + self.save_dir = str(tmp_path / "saved") + yield + shutil.rmtree(self.save_dir, ignore_errors=True) + + def test_pipeline_quarot_string(self, tiny_qwen_model_path): + """AutoRound(rotation_config='quarot') should work end-to-end on XPU.""" + ar = AutoRound( + model=tiny_qwen_model_path, + iters=0, + seqlen=8, + nsamples=2, + scheme="W4A16", + rotation_config="quarot", + device_map=DEVICE, + ) + _, quantized_model_path = ar.quantize_and_save(output_dir=self.save_dir, format="auto_round") + + model = AutoModelForCausalLM.from_pretrained(quantized_model_path, dtype="auto", device_map=DEVICE) + tokenizer = AutoTokenizer.from_pretrained(quantized_model_path) + output = generate_prompt(model, tokenizer, device=DEVICE) + assert len(output) > 0, "Quantized model should produce non-empty output" + del model + torch.xpu.empty_cache() From 65b1f74a4951cef82de4ffb7eb1f405e5b0bf574 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:53:31 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/test_xpu/transform/test_spinquant.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_xpu/transform/test_spinquant.py b/test/test_xpu/transform/test_spinquant.py index d55da7a398..0d0f20e2fe 100644 --- a/test/test_xpu/transform/test_spinquant.py +++ b/test/test_xpu/transform/test_spinquant.py @@ -135,9 +135,9 @@ def test_rotation_equivalence(self, tiny_qwen_model_path, r1, r2, r3, r4, label) logits.flatten().unsqueeze(0).float(), ).item() max_diff = (baseline_logits - logits).abs().max().item() - assert cos_sim > 0.9999, ( - f"{label} rotation broke model equivalence: cos_sim = {cos_sim:.6f}, max_diff = {max_diff:.4f}" - ) + assert ( + cos_sim > 0.9999 + ), f"{label} rotation broke model equivalence: cos_sim = {cos_sim:.6f}, max_diff = {max_diff:.4f}" class TestHookLifecycleXPU: