-
Notifications
You must be signed in to change notification settings - Fork 56
[Spyre-Next] SpyreSiluAndMul Tests #863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
9e04728
3c9870d
be40072
cbe619d
2170c8c
e4ea1e1
c1ae1a9
fae251f
c488086
c7b9935
4af0718
acf08fc
6e6fbae
bb114d2
ed78b8f
8122cd8
124f89d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| """ | ||
| Test SpyreSiluAndMul custom op correctness against a reference implementation. | ||
| """ | ||
|
|
||
| import pytest | ||
| import torch | ||
| import torch.nn.functional as F | ||
|
|
||
|
|
||
| def reference_silu_and_mul(x: torch.Tensor) -> torch.Tensor: | ||
| """Golden reference: standard SiluAndMul (SwiGLU) in PyTorch. | ||
|
|
||
| Computes: silu(x[..., :d]) * x[..., d:] where d = x.shape[-1] // 2 | ||
| """ | ||
| d = x.shape[-1] // 2 | ||
| x1 = x[..., :d] | ||
| x2 = x[..., d:] | ||
| return F.silu(x1) * x2 | ||
|
|
||
|
|
||
| @pytest.mark.spyre | ||
| @pytest.mark.siluandmul | ||
| @pytest.mark.parametrize("num_tokens", [1, 7, 63, 64, 65, 1024]) | ||
| @pytest.mark.parametrize("d", [2, 63, 64, 65, 1024, 13824]) | ||
| @pytest.mark.parametrize( | ||
| "dtype", | ||
| [ | ||
| pytest.param( | ||
| torch.float16, | ||
| marks=pytest.mark.xfail(reason="Strided tensors on float16 does not work on spyre") | ||
| ), | ||
| torch.float32, | ||
| ], | ||
| ) | ||
| def test_spyre_siluandmul_matches_reference(default_vllm_config, num_tokens, d, dtype): | ||
| """SpyreSiluAndMul output matches golden reference. | ||
|
|
||
| Tests both paths: | ||
| - forward(): custom op dispatch (no-compile path via torch.ops.vllm.spyre_siluandmul) | ||
| - forward_oot(): direct Spyre device execution | ||
| """ | ||
| from vllm_spyre_next.custom_ops.silu_and_mul import SpyreSiluAndMul | ||
|
|
||
| torch.manual_seed(42) | ||
|
|
||
| # Input shape is [num_tokens, 2*d], output shape is [num_tokens, d] | ||
| x = torch.randn(num_tokens, 2 * d, dtype=dtype) | ||
| layer = SpyreSiluAndMul() | ||
|
|
||
| expected = reference_silu_and_mul(x) | ||
| actual = layer.forward_oot(x) | ||
|
|
||
| torch.testing.assert_close(actual, expected, atol=1e-2, rtol=1e-2) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def dummy_tensor(): | ||
| return torch.randn(4, 256, dtype=torch.float32) # 2*d=256, so d=128 | ||
|
|
||
|
|
||
| def mock_forward_oot(x): | ||
| """Mock: return ones with output shape (halves last dim).""" | ||
| d = x.shape[-1] // 2 | ||
| return torch.ones(x.shape[:-1] + (d,), dtype=x.dtype, device=x.device) | ||
|
|
||
|
|
||
| @pytest.mark.spyre | ||
| @pytest.mark.siluandmul | ||
| def test_siluandmul_oot_dispatch(default_vllm_config, monkeypatch, dummy_tensor): | ||
| """Verify SiluAndMul OOT registration: class swap and forward_oot routing.""" | ||
| from vllm.model_executor.layers.activation import SiluAndMul | ||
| from vllm_spyre_next.custom_ops.silu_and_mul import SpyreSiluAndMul | ||
|
|
||
| layer = SiluAndMul() | ||
|
|
||
| # OOT class swap: SiluAndMul.__new__ should produce SpyreSiluAndMul | ||
| assert isinstance(layer, SpyreSiluAndMul) | ||
|
|
||
| # dispatch_forward should have selected forward_oot | ||
| assert layer._forward_method == layer.forward_oot | ||
|
|
||
| # Mock forward_oot (called by forward_oot) with a known transform | ||
| monkeypatch.setattr(layer, "forward_oot", mock_forward_oot) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think in the rms_test, I mocked one level lower, i.e., the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes makes sense. I will update it
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bohnstingl I have updated this |
||
| out = layer.forward_oot(dummy_tensor) | ||
|
|
||
| # Expected: ones with shape [4, 128] (halved last dim) | ||
| expected = torch.ones(4, 128, dtype=dummy_tensor.dtype) | ||
| assert torch.allclose(out, expected) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,15 +60,16 @@ def convert(tensor, device=None, dtype=None): | |
| """ | ||
| if tensor is None: | ||
| return None | ||
| if tensor.device.type == "spyre": | ||
| # In case the tensor is on spyre, we first need to move it to cpu and then change the dtype. | ||
| if device is not None: | ||
| tensor = tensor.to(device=device) | ||
| if dtype is not None: | ||
| tensor = tensor.to(dtype=dtype) | ||
| else: | ||
| if dtype is not None: | ||
| tensor = tensor.to(dtype=dtype) | ||
| if device is not None: | ||
| tensor = tensor.to(device=device) | ||
| # In case the tensor is on spyre and a dtype change is requested | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bohnstingl I have lightly modified this function here. Earlier, if this function had received a tensor residing on spyre and the device was also passed as spyre, then this function would have failed earlier.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This conversion now seems to be a little convoluted and the flow is not immediately obvious. I can't really think of a cleaner or more simplified way to do the device transfers. With your new function, the semantics changes a bit though. For example, if the tensor is on
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Isn't this expected? I relied on this comment: In the earlier definition, if I requested just a dtype change on a Spyre Tensor it would have failed. >>> from vllm_spyre_next.custom_ops.utils import convert
>>> import torch
>>> x = torch.randn((2,2), dtype=torch.float32).to(device="spyre")
>>> x
tensor([[ 1.0254, -1.2343],
[-0.9974, -0.4666]], device='spyre:0')
>>> convert(x, dtype=torch.float16)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/dev/shm/vllm-spyre/vllm_spyre_next/vllm_spyre_next/custom_ops/utils.py", line 68, in convert
tensor = tensor.to(dtype=dtype)
^^^^^^^^^^^^^^^^^^^^^^
File "/dev/shm/.venv/lib64/python3.12/site-packages/torch_spyre/_monkey_patch.py", line 63, in spyre_to
return orig_to(self, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Spyre backend does not support type conversion yet during copy. |
||
| # we first need to move it to cpu and then change the dtype. | ||
| if tensor.device.type == "spyre" and dtype is not None: | ||
| tensor = tensor.to(device="cpu") | ||
| # Set the device to spyre to avoid unexpected device change of the return tensor. | ||
| if device is None: | ||
| device = "spyre" | ||
|
|
||
| if dtype is not None: | ||
| tensor = tensor.to(dtype=dtype) | ||
| if device is not None: | ||
| tensor = tensor.to(device=device) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like I mentioned the other time, I think the logic here could be slightly improved to avoid unintended copies. Maybe something like this? |
||
| return tensor | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -526,6 +526,28 @@ def sort_key(item: pytest.Item) -> tuple[int, int]: | |
| items.sort(key=sort_key) | ||
|
|
||
|
|
||
| def _convert_yaml_value(value): | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is added for future compatibility in case we want to override the torch data types |
||
| """Convert YAML string values to Python objects where appropriate. | ||
|
|
||
| Handles torch dtype strings like "torch.half" -> torch.half. | ||
| """ | ||
| if isinstance(value, str): | ||
| import torch | ||
|
|
||
| dtype_map = { | ||
| "torch.half": torch.half, | ||
| "torch.float16": torch.float16, | ||
| "torch.bfloat16": torch.bfloat16, | ||
| "torch.float32": torch.float32, | ||
| "torch.float": torch.float, | ||
| "torch.float64": torch.float64, | ||
| "torch.double": torch.double, | ||
| } | ||
| if value in dtype_map: | ||
| return dtype_map[value] | ||
| return value | ||
|
|
||
|
|
||
| @pytest.hookimpl(tryfirst=True) | ||
| def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: | ||
| """Apply parameter overrides from YAML config.""" | ||
|
|
@@ -554,7 +576,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: | |
| for i, marker in enumerate(metafunc.definition.own_markers): | ||
| if marker.name == "parametrize" and marker.args[0] == po.param_name: | ||
| metafunc.definition.own_markers[i] = pytest.mark.parametrize( | ||
| po.param_name, list(po.values) | ||
| po.param_name, [_convert_yaml_value(v) for v in po.values] | ||
| ).mark | ||
| break | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,21 @@ tests: | |
| # hidden_size: [64] | ||
| # block_list: | ||
| # - test: "test_fused_rms_norm_quant" | ||
| # Currently the tests fail because upstream rtol and atol are 0 | ||
| # - rel_path: tests/kernels/core/test_activation.py | ||
| # allow_list: | ||
| # - test: "test_act_and_mul" | ||
| # mode: mandatory_pass | ||
| # tags: [siluandmul, upstream] | ||
| # params: | ||
| # allow: | ||
| # activation: [silu_and_mul] | ||
| # override: | ||
| # num_tokens: [1, 7, 63, 64, 65, 83, 1024] | ||
| # # Odd values fail op check. Not required for current list of models we plan to support | ||
| # d: [8, 64, 512, 13824] | ||
| # dtype: [torch.float32] | ||
| # device: [cpu] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still plan to use this eventually? To me it would also make sense to look more closely into how the changed vLLM IR does the testing? For example, the PR that recently landed in upstream vLLM introduces some new tests: https://github.com/vllm-project/vllm/pull/33825/changes#diff-510a5c10fb13605e7270f8b0647ed83a724b2658f0352af189ea1155c831ad3fR1.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Upstream tests are too strict (rtol==atol==0). Locally, I tested the upstream
Agreed! But I believe we would also some changes on our end to implement new vLLM IR compatible ops and to enabled those tests. I am following your PR #877 and will go through the attached PR in more detail and see how we can adapt them.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe! It looks like this isn't currently set up to be easily patchable: https://github.com/vllm-project/vllm/blob/e69a265135ef48312d78130f64b7bfce4cd81a37/tests/kernels/core/test_activation.py#L106 We could set up something like this that edits but we'd definitely still want to update upstream to make those parameterizable, or have them pull from a common method we can override
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bohnstingl @joerunde With this, we are able to enable the upstream
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FP16 failure is related to this internal Slack thread. In compile mode, if we send FP32 data to the and then converts the tensors to FP16. The dtype conversion makes x1, x2 contiguous. And then we transfer the tensor to the Spyre device. But if we send FP16 data to the This is an issue in |
||
| - rel_path: tests/models/language/generation/test_common.py | ||
| allow_list: | ||
| - test: "test_models" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we still need this file if we reuse the upstream tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might still need to check for
forward_ootdispatch, but other tests can go