main
py 1,062 lines 37.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for Helion kernel registration.
4
5 Tests ConfiguredHelionKernel, HelionKernelWrapper, and PresetConfigSearch
6 including config picker registration and custom autotuner integration.
7 """
8
9 from unittest.mock import Mock, patch
10
11 import pytest
12 import torch
13
14 from vllm.utils.import_utils import has_helion
15
16 if not has_helion():
17 pytest.skip(
18 "Helion is not installed. Install with: pip install vllm[helion]",
19 allow_module_level=True,
20 )
21
22 import helion
23 import helion.language as hl
24
25 from tests.kernels.helion.helpers import dummy_kernel_registry
26 from vllm.kernels.helion.case_key import CaseKey
27 from vllm.kernels.helion.config_manager import ConfigManager
28 from vllm.kernels.helion.register import (
29 _HOP_AVAILABLE,
30 ConfiguredHelionKernel,
31 HelionKernelWrapper,
32 get_kernel_by_name,
33 get_registered_kernels,
34 register_kernel,
35 validate_helion_settings,
36 )
37
38 if _HOP_AVAILABLE:
39 from helion._compat import supports_torch_compile_fusion
40 from helion._compiler._dynamo.higher_order_ops import (
41 helion_kernel_wrapper_mutation,
42 )
43 from torch._inductor.utils import run_and_get_code
44
45
46 def _add_kernel(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
47 out = torch.empty_like(x)
48 for tile in hl.tile(x.size()):
49 out[tile] = x[tile] + y[tile]
50 return out
51
52
53 @pytest.fixture
54 def sample_configs():
55 """Create real Helion config objects for testing."""
56 return {
57 CaseKey({"batchsize": 32, "hiddensize": 4096}): helion.Config(
58 block_sizes=[128],
59 num_warps=4,
60 num_stages=3,
61 ),
62 CaseKey({"batchsize": 64, "hiddensize": 4096}): helion.Config(
63 block_sizes=[256],
64 num_warps=8,
65 num_stages=4,
66 ),
67 CaseKey({"batchsize": 128, "hiddensize": 4096}): helion.Config(
68 block_sizes=[512],
69 num_warps=16,
70 num_stages=2,
71 ),
72 CaseKey.default(): helion.Config(
73 block_sizes=[64],
74 num_warps=2,
75 num_stages=2,
76 ),
77 }
78
79
80 @pytest.fixture
81 def sample_kernel():
82 """Create a simple test kernel function."""
83
84 def test_kernel(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
85 """Simple test kernel that adds two tensors."""
86 return x + y
87
88 return test_kernel
89
90
91 @pytest.fixture
92 def config_manager_with_test_configs(sample_configs):
93 """Set up ConfigManager with test configs for nvidia_h200 platform."""
94 mock_config_manager = Mock(spec=ConfigManager)
95 mock_config_manager.get_platform_configs = Mock(return_value=sample_configs)
96 return mock_config_manager
97
98
99 @pytest.fixture
100 def configured_kernel(sample_kernel, sample_configs, config_manager_with_test_configs):
101 """Create a ConfiguredHelionKernel for testing."""
102
103 def test_config_picker(args, config_keys):
104 return None
105
106 with (
107 patch(
108 "vllm.kernels.helion.config_manager.ConfigManager",
109 return_value=config_manager_with_test_configs,
110 ),
111 patch(
112 "vllm.kernels.helion.utils.get_canonical_gpu_name",
113 return_value="nvidia_h200",
114 ),
115 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
116 ):
117 mock_decorated = Mock()
118 mock_kernel.return_value = Mock(return_value=mock_decorated)
119
120 return ConfiguredHelionKernel(
121 op_name="test_kernel",
122 config_picker=test_config_picker,
123 raw_kernel_func=sample_kernel,
124 helion_settings=None,
125 )
126
127
128 class TestValidateHelionSettings:
129 """Test suite for validate_helion_settings utility function."""
130
131 def test_accepts_none_settings(self):
132 """Test that None settings are accepted without error."""
133 validate_helion_settings(None, "test_kernel") # Should not raise
134
135 def test_accepts_valid_settings(self):
136 """Test that valid settings without conflicts are accepted."""
137 settings = helion.Settings()
138 settings.static_shapes = False
139 settings.print_output_code = True
140 validate_helion_settings(settings, "test_kernel") # Should not raise
141
142 def test_rejects_autotuner_fn(self):
143 """Test that settings with custom autotuner_fn raise ValueError."""
144 settings = helion.Settings()
145 settings.autotuner_fn = lambda *args: None # Set custom autotuner function
146
147 with pytest.raises(ValueError, match="uses a custom autotuner"):
148 validate_helion_settings(settings, "test_kernel")
149
150 def test_warns_on_static_shapes_true(self):
151 """Test that static_shapes=True emits a warning about being overridden."""
152 settings = helion.Settings()
153 settings.static_shapes = True
154
155 with patch("vllm.kernels.helion.register.logger") as mock_logger:
156 validate_helion_settings(settings, "test_kernel")
157 mock_logger.warning.assert_called_once()
158 assert "overridden to False" in mock_logger.warning.call_args[0][0]
159
160
161 def create_configured_kernel_with_configs(
162 op_name,
163 config_picker,
164 kernel_func,
165 configs,
166 platform="nvidia_h200",
167 helion_settings=None,
168 ):
169 """Helper to create ConfiguredHelionKernel with real config objects."""
170 mock_config_manager = Mock(spec=ConfigManager)
171 mock_config_manager.get_platform_configs = Mock(return_value=configs)
172
173 with (
174 patch(
175 "vllm.kernels.helion.config_manager.ConfigManager",
176 return_value=mock_config_manager,
177 ),
178 patch(
179 "vllm.kernels.helion.utils.get_canonical_gpu_name",
180 return_value=platform,
181 ),
182 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
183 ):
184 mock_decorated = Mock()
185 mock_kernel.return_value = Mock(return_value=mock_decorated)
186
187 return ConfiguredHelionKernel(
188 op_name=op_name,
189 config_picker=config_picker,
190 raw_kernel_func=kernel_func,
191 helion_settings=helion_settings,
192 )
193
194
195 class TestConfiguredHelionKernel:
196 """Test suite for ConfiguredHelionKernel."""
197
198 def test_init_raises_without_picker(self, sample_kernel, sample_configs):
199 """Test that __init__ raises when no picker registered."""
200 configs: dict[CaseKey, helion.Config] = {
201 CaseKey.default(): sample_configs[CaseKey.default()]
202 }
203 mock_config_manager = Mock(spec=ConfigManager)
204 mock_config_manager.get_platform_configs = Mock(return_value=configs)
205
206 with (
207 patch(
208 "vllm.kernels.helion.config_manager.ConfigManager",
209 return_value=mock_config_manager,
210 ),
211 patch(
212 "vllm.kernels.helion.utils.get_canonical_gpu_name",
213 return_value="nvidia_h200",
214 ),
215 pytest.raises(RuntimeError, match="No config picker registered"),
216 ):
217 ConfiguredHelionKernel(
218 op_name="test_kernel",
219 config_picker=None, # No picker registered
220 raw_kernel_func=sample_kernel,
221 helion_settings=None,
222 )
223
224 def test_config_selector_validates_picker_result(
225 self, sample_kernel, sample_configs
226 ):
227 """Test that config selector validates picker returns valid key."""
228
229 def invalid_picker(args, config_keys):
230 return {"invalid": 999}
231
232 kernel = create_configured_kernel_with_configs(
233 op_name="test_kernel",
234 config_picker=invalid_picker,
235 kernel_func=sample_kernel,
236 configs=sample_configs,
237 )
238
239 key_computer = kernel._create_key_computer()
240 selector = kernel._create_config_selector(key_computer)
241
242 with pytest.raises(
243 ValueError, match="Config picker returned invalid config key"
244 ):
245 selector((torch.randn(32, 4096),))
246
247 def test_config_selector_handles_none_from_picker(
248 self, sample_kernel, sample_configs
249 ):
250 """Test that config selector falls back to 'default' on None."""
251
252 def none_picker(args, config_keys):
253 return None
254
255 kernel = create_configured_kernel_with_configs(
256 op_name="test_kernel",
257 config_picker=none_picker,
258 kernel_func=sample_kernel,
259 configs=sample_configs,
260 )
261
262 key_computer = kernel._create_key_computer()
263 selector = kernel._create_config_selector(key_computer)
264
265 result = selector((torch.randn(32, 4096),))
266 assert result is kernel.configs[CaseKey.default()]
267
268 def test_create_decorated_kernel_passes_helion_settings(
269 self, sample_kernel, sample_configs
270 ):
271 """Test that _create_decorated_kernel passes helion_settings."""
272
273 def default_picker(args, config_keys):
274 return None
275
276 settings = helion.Settings()
277 settings.print_output_code = True
278
279 mock_config_manager = Mock(spec=ConfigManager)
280 mock_config_manager.get_platform_configs = Mock(return_value=sample_configs)
281
282 with (
283 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
284 patch(
285 "vllm.kernels.helion.config_manager.ConfigManager",
286 return_value=mock_config_manager,
287 ),
288 patch(
289 "vllm.kernels.helion.utils.get_canonical_gpu_name",
290 return_value="nvidia_h200",
291 ),
292 ):
293 mock_decorated = Mock()
294 mock_kernel.return_value = Mock(return_value=mock_decorated)
295
296 ConfiguredHelionKernel(
297 op_name="test_kernel",
298 config_picker=default_picker,
299 raw_kernel_func=sample_kernel,
300 helion_settings=settings,
301 )
302
303 call_kwargs = mock_kernel.call_args[1]
304 assert "print_output_code" in call_kwargs
305 assert call_kwargs["print_output_code"] is True
306 # static_shapes is always forced to False by vLLM
307 assert call_kwargs["static_shapes"] is False
308
309 def test_key_and_config_selector_use_same_logic(
310 self, sample_kernel, sample_configs
311 ):
312 """Test that key and config_selector produce identical results."""
313
314 def tracking_picker(args, config_keys):
315 x = args[0]
316 batch_size = x.shape[0]
317 if batch_size <= 32:
318 return CaseKey({"batchsize": 32, "hiddensize": 4096})
319 elif batch_size <= 64:
320 return CaseKey({"batchsize": 64, "hiddensize": 4096})
321 return CaseKey({"batchsize": 128, "hiddensize": 4096})
322
323 mock_config_manager = Mock(spec=ConfigManager)
324 mock_config_manager.get_platform_configs = Mock(return_value=sample_configs)
325
326 with (
327 patch("vllm.kernels.helion.register.helion.kernel") as mock_helion_kernel,
328 patch(
329 "vllm.kernels.helion.config_manager.ConfigManager",
330 return_value=mock_config_manager,
331 ),
332 patch(
333 "vllm.kernels.helion.utils.get_canonical_gpu_name",
334 return_value="nvidia_h200",
335 ),
336 ):
337 mock_decorated = Mock()
338 mock_helion_kernel.return_value = Mock(return_value=mock_decorated)
339
340 kernel = ConfiguredHelionKernel(
341 op_name="test_kernel",
342 config_picker=tracking_picker,
343 raw_kernel_func=sample_kernel,
344 helion_settings=None,
345 )
346
347 call_kwargs = mock_helion_kernel.call_args[1]
348 key_fn = call_kwargs["key"]
349 autotuner_fn = call_kwargs["autotuner_fn"]
350
351 tensor = torch.randn(50, 4096) # batch=50, should select batchsize_64
352
353 key_result = key_fn(tensor)
354 autotuner = autotuner_fn(None, (tensor,))
355 config = autotuner.autotune()
356
357 expected_key = CaseKey({"batchsize": 64, "hiddensize": 4096})
358 assert key_result == str(expected_key)
359 assert config is kernel.configs[expected_key]
360
361
362 class TestHelionKernelWrapper:
363 """Test suite for HelionKernelWrapper."""
364
365 def test_init_disables_on_missing_configs(self, sample_kernel):
366 """Test __init__ marks wrapper as disabled when configs are missing."""
367
368 def fake_impl(*args, **kwargs):
369 return torch.zeros_like(args[0])
370
371 def default_picker(args, config_keys):
372 return None
373
374 mock_config_manager = Mock(spec=ConfigManager)
375 mock_config_manager.get_platform_configs = Mock(
376 return_value={}
377 ) # Empty configs
378
379 with (
380 patch(
381 "vllm.kernels.helion.config_manager.ConfigManager",
382 return_value=mock_config_manager,
383 ),
384 patch(
385 "vllm.kernels.helion.utils.get_canonical_gpu_name",
386 return_value="nvidia_h200",
387 ),
388 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
389 ):
390 mock_kernel.return_value = Mock(return_value=sample_kernel)
391
392 wrapper = HelionKernelWrapper(
393 raw_kernel_func=sample_kernel,
394 op_name="test_kernel",
395 fake_impl=fake_impl,
396 config_picker=default_picker,
397 )
398
399 assert wrapper._disabled is True
400 assert wrapper._disabled_reason is not None
401 assert "No configs available" in wrapper._disabled_reason
402
403 def test_disabled_wrapper_raises_on_call(self, sample_kernel):
404 """Test __call__ raises RuntimeError on a disabled wrapper."""
405
406 def fake_impl(*args, **kwargs):
407 return torch.zeros_like(args[0])
408
409 def default_picker(args, config_keys):
410 return None
411
412 mock_config_manager = Mock(spec=ConfigManager)
413 mock_config_manager.get_platform_configs = Mock(return_value={})
414
415 with (
416 patch(
417 "vllm.kernels.helion.config_manager.ConfigManager",
418 return_value=mock_config_manager,
419 ),
420 patch(
421 "vllm.kernels.helion.utils.get_canonical_gpu_name",
422 return_value="nvidia_h200",
423 ),
424 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
425 ):
426 mock_kernel.return_value = Mock(return_value=sample_kernel)
427
428 wrapper = HelionKernelWrapper(
429 raw_kernel_func=sample_kernel,
430 op_name="test_kernel",
431 fake_impl=fake_impl,
432 config_picker=default_picker,
433 )
434
435 with pytest.raises(RuntimeError, match="is disabled"):
436 wrapper(torch.randn(4, 4), torch.randn(4, 4))
437
438 def test_disabled_wrapper_get_configured_op_raises(self, sample_kernel):
439 """Test get_configured_op raises RuntimeError on a disabled wrapper."""
440
441 def fake_impl(*args, **kwargs):
442 return torch.zeros_like(args[0])
443
444 def default_picker(args, config_keys):
445 return None
446
447 mock_config_manager = Mock(spec=ConfigManager)
448 mock_config_manager.get_platform_configs = Mock(return_value={})
449
450 with (
451 patch(
452 "vllm.kernels.helion.config_manager.ConfigManager",
453 return_value=mock_config_manager,
454 ),
455 patch(
456 "vllm.kernels.helion.utils.get_canonical_gpu_name",
457 return_value="nvidia_h200",
458 ),
459 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
460 ):
461 mock_kernel.return_value = Mock(return_value=sample_kernel)
462
463 wrapper = HelionKernelWrapper(
464 raw_kernel_func=sample_kernel,
465 op_name="test_kernel",
466 fake_impl=fake_impl,
467 config_picker=default_picker,
468 )
469
470 with pytest.raises(RuntimeError, match="is disabled"):
471 wrapper.get_configured_op()
472
473 def test_disabled_wrapper_supports_get_inputs(self, sample_kernel):
474 """Test get_inputs works on a disabled wrapper."""
475
476 def fake_impl(*args, **kwargs):
477 return torch.zeros_like(args[0])
478
479 def default_picker(args, config_keys):
480 return None
481
482 expected_inputs = {"key1": (torch.randn(4),)}
483 input_gen = Mock(return_value=expected_inputs)
484
485 mock_config_manager = Mock(spec=ConfigManager)
486 mock_config_manager.get_platform_configs = Mock(return_value={})
487
488 with (
489 patch(
490 "vllm.kernels.helion.config_manager.ConfigManager",
491 return_value=mock_config_manager,
492 ),
493 patch(
494 "vllm.kernels.helion.utils.get_canonical_gpu_name",
495 return_value="nvidia_h200",
496 ),
497 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
498 ):
499 mock_kernel.return_value = Mock(return_value=sample_kernel)
500
501 wrapper = HelionKernelWrapper(
502 raw_kernel_func=sample_kernel,
503 op_name="test_kernel",
504 fake_impl=fake_impl,
505 config_picker=default_picker,
506 input_generator=input_gen,
507 )
508
509 assert wrapper._disabled is True
510 result = wrapper.get_inputs()
511 assert result is expected_inputs
512
513 def test_disabled_wrapper_supports_run_autotune(self, sample_kernel):
514 """Test run_autotune works on a disabled wrapper."""
515
516 def fake_impl(*args, **kwargs):
517 return torch.zeros_like(args[0])
518
519 def default_picker(args, config_keys):
520 return None
521
522 mock_config_manager = Mock(spec=ConfigManager)
523 mock_config_manager.get_platform_configs = Mock(return_value={})
524
525 mock_config = Mock()
526
527 with (
528 patch(
529 "vllm.kernels.helion.config_manager.ConfigManager",
530 return_value=mock_config_manager,
531 ),
532 patch(
533 "vllm.kernels.helion.utils.get_canonical_gpu_name",
534 return_value="nvidia_h200",
535 ),
536 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
537 ):
538 mock_kernel.return_value = Mock(return_value=sample_kernel)
539
540 wrapper = HelionKernelWrapper(
541 raw_kernel_func=sample_kernel,
542 op_name="test_kernel",
543 fake_impl=fake_impl,
544 config_picker=default_picker,
545 )
546
547 assert wrapper._disabled is True
548
549 with patch(
550 "vllm.kernels.helion.register.create_helion_decorated_kernel"
551 ) as mock_create:
552 mock_autotune_kernel = Mock()
553 mock_autotune_kernel.autotune.return_value = mock_config
554 mock_create.return_value = mock_autotune_kernel
555
556 inputs = (torch.randn(4, 4),)
557 result = wrapper.run_autotune(inputs)
558 assert result is mock_config
559
560 def test_init_caches_configured_kernel(self, sample_kernel, sample_configs):
561 """Test __init__ eagerly builds and caches ConfiguredHelionKernel."""
562
563 def fake_impl(*args, **kwargs):
564 return torch.zeros_like(args[0])
565
566 def default_picker(args, config_keys):
567 return None
568
569 mock_config_manager = Mock(spec=ConfigManager)
570 mock_config_manager.get_platform_configs = Mock(return_value=sample_configs)
571
572 with (
573 patch(
574 "vllm.kernels.helion.config_manager.ConfigManager",
575 return_value=mock_config_manager,
576 ),
577 patch(
578 "vllm.kernels.helion.utils.get_canonical_gpu_name",
579 return_value="nvidia_h200",
580 ),
581 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
582 ):
583 mock_kernel.return_value = Mock(return_value=sample_kernel)
584
585 wrapper = HelionKernelWrapper(
586 raw_kernel_func=sample_kernel,
587 op_name="test_kernel",
588 fake_impl=fake_impl,
589 config_picker=default_picker,
590 )
591
592 assert wrapper._configured_kernel is not None
593 result1 = wrapper.get_configured_op()
594 result2 = wrapper.get_configured_op()
595 assert result1 is result2
596
597 @pytest.mark.skipif(
598 not _HOP_AVAILABLE, reason="HOP path only used when HOP available"
599 )
600 def test_init_eagerly_initializes_hop_path(self):
601 """Test that register_kernel eagerly builds the configured kernel
602 on the HOP path (no custom op registration needed)."""
603 from vllm.kernels.helion.utils import get_canonical_gpu_name
604
605 configs: dict[CaseKey, helion.Config] = {
606 CaseKey.default(): helion.Config(block_sizes=[4, 4])
607 }
608 with (
609 dummy_kernel_registry(configs=configs) as register,
610 patch(
611 "vllm.kernels.helion.utils.get_canonical_gpu_name",
612 wraps=get_canonical_gpu_name,
613 ) as mock_gpu,
614 ):
615 wrapper = register(
616 config_picker=lambda args, keys: None,
617 )(_add_kernel)
618
619 mock_gpu.assert_called_once()
620 assert wrapper._configured_kernel is not None
621
622 with patch(
623 "vllm.kernels.helion.utils.get_canonical_gpu_name",
624 side_effect=AssertionError("get_canonical_gpu_name called during __call__"),
625 ):
626 x = torch.randn(4, 4, device="cuda")
627 y = torch.randn(4, 4, device="cuda")
628 result = wrapper(x, y)
629 expected = x + y
630 assert torch.allclose(result, expected)
631
632 @pytest.mark.skipif(
633 _HOP_AVAILABLE, reason="CustomOp path not used when HOP available"
634 )
635 def test_init_eagerly_initializes(self):
636 """Test that register_kernel eagerly loads configs and detects GPU
637 during construction so __call__ needs no further initialization."""
638 from vllm.kernels.helion.utils import get_canonical_gpu_name
639
640 with (
641 dummy_kernel_registry() as register,
642 patch(
643 "vllm.kernels.helion.utils.get_canonical_gpu_name",
644 wraps=get_canonical_gpu_name,
645 ) as mock_gpu,
646 ):
647 wrapper = register(
648 config_picker=lambda args, keys: None,
649 )(_add_kernel)
650
651 # Init must have detected GPU and built the kernel
652 mock_gpu.assert_called_once()
653 assert wrapper._configured_kernel is not None
654 assert hasattr(torch.ops.vllm_helion, wrapper.op_name)
655
656 @pytest.mark.skipif(
657 _HOP_AVAILABLE, reason="CustomOp path not used when HOP available"
658 )
659 def test_get_or_register_custom_op_returns_cached_op(
660 self, sample_kernel, sample_configs
661 ):
662 def fake_impl(*args, **kwargs):
663 return torch.zeros_like(args[0])
664
665 def default_picker(args, config_keys):
666 return None
667
668 mock_config_manager = Mock(spec=ConfigManager)
669 mock_config_manager.get_platform_configs = Mock(return_value=sample_configs)
670
671 existing_op = Mock()
672 mock_namespace = Mock()
673 mock_namespace.test_kernel = existing_op
674
675 with (
676 patch(
677 "vllm.kernels.helion.config_manager.ConfigManager",
678 return_value=mock_config_manager,
679 ),
680 patch(
681 "vllm.kernels.helion.utils.get_canonical_gpu_name",
682 return_value="nvidia_h200",
683 ),
684 patch.object(torch.ops, "vllm_helion", mock_namespace),
685 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
686 ):
687 mock_decorated = Mock()
688 mock_kernel.return_value = Mock(return_value=mock_decorated)
689
690 wrapper = HelionKernelWrapper(
691 raw_kernel_func=sample_kernel,
692 op_name="test_kernel",
693 fake_impl=fake_impl,
694 config_picker=default_picker,
695 )
696 result = wrapper._get_or_register_custom_op()
697 assert result is existing_op
698
699 @pytest.mark.skipif(
700 _HOP_AVAILABLE, reason="CustomOp path not used when HOP available"
701 )
702 def test_get_or_register_custom_op_registers_new_op(
703 self, sample_kernel, sample_configs
704 ):
705 def fake_impl(*args, **kwargs):
706 return torch.zeros_like(args[0])
707
708 def default_picker(args, config_keys):
709 return None
710
711 mock_config_manager = Mock(spec=ConfigManager)
712 mock_config_manager.get_platform_configs = Mock(return_value=sample_configs)
713
714 new_op = Mock()
715 registered_ops: dict[str, Mock] = {}
716 mutates_args = ["y"]
717
718 class MockNamespace:
719 def __getattr__(self, name):
720 if name in registered_ops:
721 return registered_ops[name]
722 raise AttributeError(name)
723
724 mock_namespace = MockNamespace()
725
726 def register_side_effect(op_name, op_func, **kwargs):
727 registered_ops[op_name] = new_op
728
729 with (
730 patch(
731 "vllm.kernels.helion.config_manager.ConfigManager",
732 return_value=mock_config_manager,
733 ),
734 patch(
735 "vllm.kernels.helion.utils.get_canonical_gpu_name",
736 return_value="nvidia_h200",
737 ),
738 patch.object(torch.ops, "vllm_helion", mock_namespace),
739 patch(
740 "vllm.kernels.helion.register.direct_register_custom_op",
741 side_effect=register_side_effect,
742 ) as mock_register,
743 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
744 ):
745 mock_decorated = Mock()
746 mock_kernel.return_value = Mock(return_value=mock_decorated)
747
748 wrapper = HelionKernelWrapper(
749 raw_kernel_func=sample_kernel,
750 op_name="test_kernel",
751 fake_impl=fake_impl,
752 mutates_args=mutates_args,
753 config_picker=default_picker,
754 )
755 result = wrapper._get_or_register_custom_op()
756
757 mock_register.assert_called_once()
758 assert result is new_op
759 assert mock_register.call_args[1]["op_func"] is mock_decorated
760 assert mock_register.call_args[1]["mutates_args"] is mutates_args
761
762
763 class TestKernelRegistry:
764 """Test suite for kernel registry functionality."""
765
766 def setup_method(self):
767 """Save and clear the registry before each test."""
768 from vllm.kernels.helion.register import _REGISTERED_KERNELS
769
770 self._saved_registry = dict(_REGISTERED_KERNELS)
771 _REGISTERED_KERNELS.clear()
772
773 def teardown_method(self):
774 """Restore the registry after each test."""
775 from vllm.kernels.helion.register import _REGISTERED_KERNELS
776
777 _REGISTERED_KERNELS.clear()
778 _REGISTERED_KERNELS.update(self._saved_registry)
779
780 def test_get_registered_kernels_returns_copy(self):
781 """Test get_registered_kernels returns copy of registry."""
782 result1 = get_registered_kernels()
783 result2 = get_registered_kernels()
784
785 # Should be separate objects
786 assert result1 is not result2
787 # Should have same content
788 assert result1 == result2
789
790 def test_get_kernel_by_name_returns_kernel(self):
791 """Test get_kernel_by_name returns registered kernel."""
792 with dummy_kernel_registry() as register:
793 wrapper = register("test_kernel", config_picker=lambda args, keys: None)(
794 _add_kernel
795 )
796
797 from vllm.kernels.helion.register import _REGISTERED_KERNELS
798
799 _REGISTERED_KERNELS["test_kernel"] = wrapper
800
801 result = get_kernel_by_name("test_kernel")
802 assert result is wrapper
803
804 def test_get_kernel_by_name_returns_none_for_missing(self):
805 """Test get_kernel_by_name returns None for missing kernel."""
806 result = get_kernel_by_name("nonexistent")
807 assert result is None
808
809 def test_register_kernel_auto_generates_fake_impl(self):
810 """Test register_kernel auto-generates fake_impl when not provided."""
811 with (
812 dummy_kernel_registry() as register,
813 patch("vllm.kernels.helion.register.infer_fake_impl") as mock_infer,
814 ):
815 mock_fake = Mock()
816 mock_infer.return_value = mock_fake
817 wrapper = register(
818 config_picker=lambda args, keys: None,
819 )(_add_kernel)
820
821 mock_infer.assert_called_once_with(_add_kernel, None)
822 assert wrapper._fake_impl is mock_fake
823
824 def test_register_kernel_creates_wrapper(self):
825 """Test register_kernel creates HelionKernelWrapper."""
826 with dummy_kernel_registry() as register:
827 result = register("test_name", config_picker=lambda args, keys: None)(
828 _add_kernel
829 )
830
831 assert isinstance(result, HelionKernelWrapper)
832 assert result.op_name == "test_name"
833 assert result.raw_kernel_func is _add_kernel
834
835 def test_register_kernel_auto_detects_name(self):
836 """Test register_kernel uses function name when no name provided."""
837 with dummy_kernel_registry() as register:
838 wrapper = register(config_picker=lambda args, keys: None)(_add_kernel)
839
840 assert wrapper.op_name == "_add_kernel"
841
842 def test_register_kernel_registers_in_global_registry(self):
843 """Test register_kernel adds wrapper to global registry."""
844 with dummy_kernel_registry() as register:
845 wrapper = register("test_kernel", config_picker=lambda args, keys: None)(
846 _add_kernel
847 )
848
849 registered_kernels = get_registered_kernels()
850 assert "test_kernel" in registered_kernels
851 assert registered_kernels["test_kernel"] is wrapper
852
853 def test_register_kernel_passes_helion_settings(self):
854 """Test register_kernel passes helion_settings to wrapper."""
855 settings = helion.Settings()
856 settings.print_output_code = True
857
858 with dummy_kernel_registry() as register:
859 result = register(
860 "test_name",
861 config_picker=lambda args, keys: None,
862 helion_settings=settings,
863 )(_add_kernel)
864
865 assert result.helion_settings is settings
866
867 def test_register_kernel_supports_decorator_syntax(self):
868 """Test register_kernel works with decorator arguments."""
869 mock_fake = Mock()
870
871 with dummy_kernel_registry() as register:
872 result = register(
873 "custom_name",
874 config_picker=lambda args, keys: None,
875 fake_impl=mock_fake,
876 )(_add_kernel)
877
878 assert result.op_name == "custom_name"
879 assert result._fake_impl is mock_fake
880
881 def test_register_kernel_raises_on_duplicate_registration(self):
882 """Test register_kernel raises error on duplicate names."""
883 with dummy_kernel_registry() as register:
884 register("duplicate_name", config_picker=lambda args, keys: None)(
885 _add_kernel
886 )
887
888 with pytest.raises(ValueError, match="already registered"):
889 register("duplicate_name", config_picker=lambda args, keys: None)(
890 _add_kernel
891 )
892
893 def test_register_kernel_rejects_autotuner_fn_in_settings(self):
894 """Test register_kernel rejects conflicting autotuner_fn."""
895 mock_settings = Mock()
896 mock_settings.to_dict.return_value = {"autotuner_fn": Mock()}
897
898 with pytest.raises(ValueError, match="uses a custom autotuner"):
899
900 @register_kernel(
901 "test",
902 config_picker=lambda args, keys: None,
903 helion_settings=mock_settings,
904 )
905 def test_kernel(x):
906 return x
907
908 def test_register_kernel_no_warning_with_static_shapes_false(self):
909 """Test register_kernel doesn't warn with static_shapes=False."""
910 mock_settings = Mock()
911 mock_settings.to_dict.return_value = {"static_shapes": False}
912
913 with (
914 dummy_kernel_registry() as register,
915 patch("vllm.kernels.helion.register.logger") as mock_logger,
916 ):
917 register(
918 "test",
919 config_picker=lambda args, keys: None,
920 helion_settings=mock_settings,
921 )(_add_kernel)
922
923 mock_logger.warning.assert_not_called()
924
925 def test_disabled_kernel_appears_in_registry(self):
926 """Test that a disabled wrapper is still in the global registry."""
927
928 def fake_impl(*args, **kwargs):
929 return torch.zeros_like(args[0])
930
931 mock_config_manager = Mock(spec=ConfigManager)
932 mock_config_manager.get_platform_configs = Mock(return_value={})
933
934 with (
935 patch(
936 "vllm.kernels.helion.config_manager.ConfigManager",
937 return_value=mock_config_manager,
938 ),
939 patch(
940 "vllm.kernels.helion.utils.get_canonical_gpu_name",
941 return_value="nvidia_h200",
942 ),
943 patch("vllm.kernels.helion.register.helion.kernel") as mock_kernel,
944 ):
945 mock_kernel.return_value = Mock(return_value=_add_kernel)
946
947 wrapper = register_kernel(
948 "disabled_kernel",
949 config_picker=lambda args, keys: None,
950 fake_impl=fake_impl,
951 )(_add_kernel)
952
953 assert wrapper._disabled is True
954 registered = get_registered_kernels()
955 assert "disabled_kernel" in registered
956 assert registered["disabled_kernel"] is wrapper
957
958
959 @pytest.mark.skipif(not _HOP_AVAILABLE, reason="Requires PyTorch >= 2.11 for HOP")
960 class TestTorchCompileHOP:
961 """Test that HelionKernelWrapper emits the correct HOP under torch.compile."""
962
963 def test_compiled_graph_contains_helion_hop(self):
964 """Verify torch.compile on a HelionKernelWrapper emits a
965 helion_kernel_wrapper_mutation HOP node in the FX graph."""
966 configs: dict[CaseKey, helion.Config] = {
967 CaseKey.default(): helion.Config(block_sizes=[4, 4])
968 }
969
970 with dummy_kernel_registry(configs=configs) as register:
971 add_helion_kernel = register(
972 op_name="test_torch_compile_add_kernel",
973 config_picker=lambda args, keys: None,
974 )(_add_kernel)
975
976 captured_graph: torch.fx.GraphModule | None = None
977
978 def capturing_backend(gm, example_inputs):
979 nonlocal captured_graph
980 assert captured_graph is None, "Backend called multiple times"
981 captured_graph = gm
982 return gm.forward
983
984 def f(x, y):
985 return add_helion_kernel(x, y)
986
987 torch._dynamo.reset()
988 compiled_f = torch.compile(f, backend=capturing_backend, fullgraph=True)
989
990 x = torch.randn(4, 4, device="cuda")
991 y = torch.randn(4, 4, device="cuda")
992
993 # Run compiled version and capture graph
994 compiled_result = compiled_f(x, y)
995
996 assert captured_graph is not None
997 hop_nodes = [
998 node
999 for node in captured_graph.graph.nodes
1000 if node.op == "call_function"
1001 and node.target is helion_kernel_wrapper_mutation
1002 ]
1003 assert len(hop_nodes) > 0, (
1004 "Expected helion_kernel_wrapper_mutation HOP node in compiled graph, "
1005 f"but found none. Graph nodes: "
1006 f"{[(n.op, n.target) for n in captured_graph.graph.nodes]}"
1007 )
1008
1009 # Verify compiled result matches eager execution
1010 eager_result = f(x, y) # Run in eager mode
1011
1012 assert torch.allclose(compiled_result, eager_result, atol=1e-5, rtol=1e-5), (
1013 "Compiled execution result doesn't match eager execution. "
1014 f"Max difference: {torch.max(torch.abs(compiled_result - eager_result))}"
1015 )
1016
1017 @pytest.mark.skipif(
1018 not (_HOP_AVAILABLE and supports_torch_compile_fusion()),
1019 reason="Requires PyTorch with Helion inductor fusion support",
1020 )
1021 def test_inductor_backend_compiles_helion_hop(self):
1022 """Test torch.compile with inductor backend and Helion fusion enabled."""
1023 configs: dict[CaseKey, helion.Config] = {
1024 CaseKey.default(): helion.Config(block_sizes=[4, 4])
1025 }
1026
1027 with dummy_kernel_registry(configs=configs) as register:
1028 add_helion_kernel = register(
1029 op_name="test_inductor_add_kernel",
1030 config_picker=lambda args, keys: None,
1031 helion_settings=helion.Settings(
1032 torch_compile_fusion=True, static_shapes=False
1033 ),
1034 )(_add_kernel)
1035
1036 def f(x, y):
1037 x = x * 2.0
1038 y = y + 1.0
1039 out = add_helion_kernel(x, y)
1040 return out.relu()
1041
1042 torch._dynamo.reset()
1043 compiled_f = torch.compile(f, backend="inductor", fullgraph=True)
1044
1045 x = torch.randn(4, 4, device="cuda")
1046 y = torch.randn(4, 4, device="cuda")
1047
1048 compiled_result, source_codes = run_and_get_code(compiled_f, x, y)
1049 eager_result = f(x, y)
1050
1051 assert torch.allclose(compiled_result, eager_result, atol=1e-5, rtol=1e-5), (
1052 "Inductor-compiled result doesn't match eager execution. "
1053 f"Max difference: {torch.max(torch.abs(compiled_result - eager_result))}"
1054 )
1055
1056 # With fusion enabled, prologue/epilogue ops should be fused into
1057 # a single triton kernel rather than generating separate kernels.
1058 kernel_count = sum(code.count("@triton.jit") for code in source_codes)
1059 assert kernel_count == 1, (
1060 f"Expected 1 fused triton kernel, got {kernel_count}. "
1061 "Prologue/epilogue ops were not fused into the Helion kernel."
1062 )