Repository: huggingface/transformers
Pull request: fix: split MXFP4 dependency checks for specific error messages

## Summary

- Fixes #44912 — MXFP4 quantization error messages combine `is_triton_available()` and `is_kernels_available()` into a single `kernels_available` boolean, making it impossible to identify which dependency is missing
- Split the combined check into separate `triton_available` and `kernels_installed` variables with individual, actionable error messages including install instructions
- Hardened `is_triton_available()` and `is_kernels_available()` in `import_utils.py` to handle `"N/A"` version strings gracefully (returns `False` instead of crashing with `InvalidVersion`)
- Follows the established pattern in `quantizer_higgs.py` where each dependency is checked individually

## Test plan

- [x] Added `test_warning_missing_kernels_only` — verifies warning specifically mentions `kernels` when only kernels is missing
- [x] Added `test_warning_missing_triton_only` — verifies warning specifically mentions `triton` when only triton is missing
- [x] Added `test_error_missing_kernels_not_prequantized` — verifies `ValueError` mentions `kernels`
- [x] Added `test_error_missing_triton_not_prequantized` — verifies `ValueError` mentions `triton`
- [x] Added `test_is_kernels_available_with_na_version` — handles `"N/A"` version without crash
- [x] Added `test_is_triton_available_with_na_version` — handles `"N/A"` version without crash
- [x] All existing MXFP4 tests pass (23 passed, 8 skipped)
- [x] `ruff format --check` and `ruff check` clean on modified files (3 pre-existing warnings in untouched code)

Changed files:
filename: src/transformers/quantizers/quantizer_mxfp4.py
patch: @@ -90,44 +90,61 @@ def validate_environment(self, *args, **kwargs):
 
         if torch.xpu.is_available():
             is_device_supported_mxfp4 = True
-            kernels_available = is_triton_available("3.5.0") and is_kernels_available()
+            triton_available = is_triton_available("3.5.0")
+            kernels_installed = is_kernels_available()
         elif torch.cuda.is_available():
             compute_capability = torch.cuda.get_device_capability()
             is_device_supported_mxfp4 = compute_capability >= (7, 5)
-            kernels_available = is_triton_available("3.4.0") and is_kernels_available()
+            triton_available = is_triton_available("3.4.0")
+            kernels_installed = is_kernels_available()
         elif device.type == "cpu":
             is_device_supported_mxfp4 = True
-            kernels_available = is_triton_available("3.5.0") and is_kernels_available()
+            triton_available = is_triton_available("3.5.0")
+            kernels_installed = is_kernels_available()
         else:
             is_device_supported_mxfp4 = False
-            kernels_available = False
+            triton_available = False
+            kernels_installed = False
 
         if self.pre_quantized:
-            # On unsupported GPUs or without kernels, we will dequantize the model to bf16
             if not is_device_supported_mxfp4:
                 logger.warning_once(
-                    "MXFP4 quantization is only supported on GPUs with compute capability >= 7.5 (e.g T4, A100, L4, H100, or B200) or XPUs (e.g Intel® Data Center GPU Max Series) "
+                    "MXFP4 quantization is only supported on GPUs with compute capability >= 7.5 "
+                    "(e.g T4, A100, L4, H100, or B200) or XPUs (e.g Intel® Data Center GPU Max Series). "
                     "We will default to dequantizing the model to bf16."
                 )
                 self.quantization_config.dequantize = True
                 return
 
-            if not kernels_available:
+            if not triton_available:
                 logger.warning_once(
-                    "MXFP4 quantization requires Triton and kernels installed: CUDA requires Triton >= 3.4.0, XPU requires Triton >= 3.5.0, we will default to dequantizing the model to bf16"
+                    "MXFP4 quantization requires Triton: CUDA requires Triton >= 3.4.0, "
+                    "XPU/CPU requires Triton >= 3.5.0. Please install triton: `pip install triton`. "
+                    "We will default to dequantizing the model to bf16."
+                )
+                self.quantization_config.dequantize = True
+                return
+
+            if not kernels_installed:
+                logger.warning_once(
+                    "MXFP4 quantization requires the `kernels` package: "
+                    "`pip install kernels>=0.12.0`. "
+                    "We will default to dequantizing the model to bf16."
                 )
                 self.quantization_config.dequantize = True
                 return
         elif not is_device_supported_mxfp4:
-            # we can't quantize the model in this case so we raise an error
             raise ValueError(
-                "MXFP4 quantization is only supported on GPUs with compute capability >= 7.5 (e.g T4, A100, L4, H100, or B200) or XPUs (e.g Intel® Data Center GPU Max Series) or CPU"
+                "MXFP4 quantization is only supported on GPUs with compute capability >= 7.5 "
+                "(e.g T4, A100, L4, H100, or B200) or XPUs (e.g Intel® Data Center GPU Max Series) or CPU"
             )
-        elif not kernels_available:
-            # we can't quantize the model in this case so we raise an error
+        elif not triton_available:
             raise ValueError(
-                "MXFP4 quantization requires Triton and kernels installed: CUDA requires Triton >= 3.4.0, XPU/CPU requires Triton >= 3.5.0"
+                "MXFP4 quantization requires Triton: CUDA requires Triton >= 3.4.0, "
+                "XPU/CPU requires Triton >= 3.5.0. Please install triton: `pip install triton`"
             )
+        elif not kernels_installed:
+            raise ValueError("MXFP4 quantization requires the `kernels` package: `pip install kernels>=0.12.0`")
 
         if not self.pre_quantized:
             self._lazy_import_kernels()

filename: tests/quantization/mxfp4/test_mxfp4.py
patch: @@ -112,6 +112,9 @@ class Mxfp4QuantizerTest(unittest.TestCase):
     def setUp(self):
         gc.collect()
         _empty_accelerator_cache()
+        from transformers.utils.logging import warning_once
+
+        warning_once.cache_clear()
 
     def test_quantizer_validation_no_torch(self):
         """Test quantizer validation when torch is not available"""
@@ -248,6 +251,76 @@ def test_is_trainable(self):
         # MXFP4 is not trainable
         self.assertFalse(quantizer.is_trainable)
 
+    @require_torch_gpu
+    def test_warning_distinguishes_triton_from_kernels(self):
+        """When only one dependency is missing, warning should mention it specifically."""
+        from transformers.quantizers.quantizer_mxfp4 import Mxfp4HfQuantizer
+
+        # Missing kernels only -> warning should mention kernels
+        config = Mxfp4Config()
+        quantizer = Mxfp4HfQuantizer(config)
+        quantizer.pre_quantized = True
+
+        with (
+            patch("transformers.quantizers.quantizer_mxfp4.is_triton_available", return_value=True),
+            patch("transformers.quantizers.quantizer_mxfp4.is_kernels_available", return_value=False),
+            self.assertLogs("transformers", level="WARNING") as cm,
+        ):
+            quantizer.validate_environment()
+
+        warning_text = " ".join(cm.output)
+        self.assertIn("kernels", warning_text.lower())
+        self.assertTrue(quantizer.quantization_config.dequantize)
+
+        # Missing triton only -> warning should mention triton
+        config = Mxfp4Config()
+        quantizer = Mxfp4HfQuantizer(config)
+        quantizer.pre_quantized = True
+
+        with (
+            patch("transformers.quantizers.quantizer_mxfp4.is_triton_available", return_value=False),
+            patch("transformers.quantizers.quantizer_mxfp4.is_kernels_available", return_value=True),
+            self.assertLogs("transformers", level="WARNING") as cm,
+        ):
+            quantizer.validate_environment()
+
+        warning_text = " ".join(cm.output)
+        self.assertIn("triton", warning_text.lower())
+        self.assertTrue(quantizer.quantization_config.dequantize)
+
+    @require_torch_gpu
+    def test_error_distinguishes_triton_from_kernels(self):
+        """When quantizing without a dependency, ValueError should mention it specifically."""
+        from transformers.quantizers.quantizer_mxfp4 import Mxfp4HfQuantizer
+
+        # Missing kernels only -> error should mention kernels
+        config = Mxfp4Config()
+        quantizer = Mxfp4HfQuantizer(config)
+        quantizer.pre_quantized = False
+
+        with (
+            patch("transformers.quantizers.quantizer_mxfp4.is_triton_available", return_value=True),
+            patch("transformers.quantizers.quantizer_mxfp4.is_kernels_available", return_value=False),
+        ):
+            with self.assertRaises(ValueError) as ctx:
+                quantizer.validate_environment()
+
+        self.assertIn("kernels", str(ctx.exception).lower())
+
+        # Missing triton only -> error should mention triton
+        config = Mxfp4Config()
+        quantizer = Mxfp4HfQuantizer(config)
+        quantizer.pre_quantized = False
+
+        with (
+            patch("transformers.quantizers.quantizer_mxfp4.is_triton_available", return_value=False),
+            patch("transformers.quantizers.quantizer_mxfp4.is_kernels_available", return_value=True),
+        ):
+            with self.assertRaises(ValueError) as ctx:
+                quantizer.validate_environment()
+
+        self.assertIn("triton", str(ctx.exception).lower())
+
 
 class Mxfp4IntegrationTest(unittest.TestCase):
     """Test mxfp4 integration functions"""