main
py 120 lines 4.33 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 import torch
6
7 import vllm
8 from tests.compile.backend import TestBackend
9 from vllm.compilation.passes.utility.noop_elimination import NoOpEliminationPass
10 from vllm.config import CompilationConfig, CompilationMode, PassConfig, VllmConfig
11 from vllm.platforms import current_platform
12
13 DEVICE_TYPE = current_platform.device_type
14
15
16 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
17 # Important edge case is when `num_tokens == buffer_size`
18 @pytest.mark.parametrize(
19 ("num_tokens", "buffer_size"), [(256, 256), (256, 512), (1024, 1024), (1024, 1025)]
20 )
21 @pytest.mark.parametrize("hidden_size", [64, 4096])
22 def test_noop_elimination(dtype, num_tokens, hidden_size, buffer_size):
23 torch.set_default_device(DEVICE_TYPE)
24 torch.set_default_dtype(dtype)
25 torch.manual_seed(1)
26
27 class Model(torch.nn.Module):
28 def __init__(self) -> None:
29 super().__init__()
30 # Avoid using empty, since on rocm torch.empty
31 # does not initialize the memory.
32 self.pos_embed = torch.randn(buffer_size, hidden_size, dtype=dtype)
33
34 def forward(self, x):
35 # Avoid += to prevent inplace addition.
36 x = x + self.pos_embed[: x.shape[0]]
37 # Chain of reshapes
38 y = x.reshape(-1, 128, 32)
39 z = y.reshape(-1, 4096)
40 # No-op reshape
41 a = z.reshape(-1, 4096)
42 # Final reshape that should remain
43 b = a.reshape(-1, 128, 32)
44 # No-op slice
45 c = b[0 : b.shape[0]]
46 # The pass should replace the result of this op with `c`.
47 d = torch.slice_scatter(
48 torch.ones_like(c), # Dummy tensor to be scattered into
49 c, # Source tensor
50 0, # dim
51 0, # start
52 c.shape[0], # end
53 )
54 return d
55
56 vllm_config = VllmConfig(
57 compilation_config=CompilationConfig(
58 mode=CompilationMode.VLLM_COMPILE,
59 pass_config=PassConfig(eliminate_noops=True),
60 )
61 )
62 with vllm.config.set_current_vllm_config(vllm_config):
63 noop_pass = NoOpEliminationPass(vllm_config)
64
65 backend = TestBackend(noop_pass)
66
67 model = Model()
68 # First dimension dynamic
69 x = torch.rand(num_tokens, hidden_size)
70 torch._dynamo.mark_dynamic(x, 0)
71
72 result = model(x)
73
74 model2 = torch.compile(model, backend=backend)
75 result2 = model2(x)
76
77 ATOL, RTOL = (2e-3, 2e-3)
78 torch.testing.assert_close(result, result2, atol=ATOL, rtol=RTOL)
79
80 # The no-op reshape and slice should be eliminated.
81 # The initial slice on the positional embedding should remain.
82 # The chain of reshapes should be fused into a single reshape.
83 assert backend.op_count(torch.ops.aten.reshape.default) == 1
84 assert backend.op_count(torch.ops.aten.slice.Tensor) == 1
85 assert backend.op_count(torch.ops.aten.slice_scatter.default) == 0
86
87
88 def test_non_noop_slice_preserved():
89 """Ensure that a slice with end=-1 (dropping last row) is NOT eliminated.
90
91 Regression test for a bug where end=-1 was treated like an inferred
92 dimension (reshape semantics) leading to incorrect elimination.
93 """
94 torch.set_default_device(DEVICE_TYPE)
95 x = torch.randn(16, 16)
96
97 class SliceModel(torch.nn.Module):
98 def forward(self, x):
99 base = x.clone()
100 src = torch.ones(15, 16)
101 y = torch.slice_scatter(base, src, dim=0, start=0, end=-1)
102 return x[0:-1, :], y
103
104 vllm_config = VllmConfig(
105 compilation_config=CompilationConfig(
106 mode=CompilationMode.VLLM_COMPILE,
107 pass_config=PassConfig(eliminate_noops=True),
108 )
109 )
110 with vllm.config.set_current_vllm_config(vllm_config):
111 noop_pass = NoOpEliminationPass(vllm_config)
112 backend = TestBackend(noop_pass)
113 model = SliceModel()
114 ref = model(x)
115 compiled = torch.compile(model, backend=backend)
116 out = compiled(x)
117 torch.testing.assert_close(ref, out)
118 # The slice should remain (not a no-op).
119 assert backend.op_count(torch.ops.aten.slice.Tensor) == 1
120 assert backend.op_count(torch.ops.aten.slice_scatter.default) == 1