@@ -23,6 +23,44 @@ def _attention_spec(head_size: int, head_size_v: int | None = None):
2323 )
2424
2525
26+ class _FakeFlashInferWrapper :
27+ def __init__ (
28+ self ,
29+ float_workspace_buffer : torch .Tensor | None = None ,
30+ int_workspace_bytes : int = 1 ,
31+ ) -> None :
32+ self ._float_workspace_buffer = (
33+ float_workspace_buffer
34+ if float_workspace_buffer is not None
35+ else torch .empty (1 , dtype = torch .uint8 )
36+ )
37+ self ._int_workspace_buffer = torch .empty (
38+ max (int_workspace_bytes , 1 ), dtype = torch .uint8
39+ )
40+ self ._vllm_flashinfer_int_workspace_finalized = False
41+ self .is_cuda_graph_enabled = False
42+ self .reset_calls = 0
43+
44+ def reset_workspace_buffer (
45+ self ,
46+ float_workspace_buffer : torch .Tensor ,
47+ int_workspace_buffer : torch .Tensor ,
48+ ) -> None :
49+ self ._float_workspace_buffer = float_workspace_buffer
50+ self ._int_workspace_buffer = int_workspace_buffer
51+ self .reset_calls += 1
52+
53+
54+ def _make_flashinfer_builder (flashinfer_backend ):
55+ FlashInferMetadataBuilder = flashinfer_backend .FlashInferMetadataBuilder
56+ builder = FlashInferMetadataBuilder .__new__ (FlashInferMetadataBuilder )
57+ builder ._workspace_buffer = None
58+ builder ._workspace_state = flashinfer_backend ._FlashInferWorkspaceState ()
59+ builder .device = torch .device ("cpu" )
60+ builder .use_dcp = False
61+ return builder
62+
63+
2664def test_flashinfer_separate_cudagraph_memory_profile_gate ():
2765 pytest .importorskip ("flashinfer" )
2866 from vllm .v1 .attention .backends .flashinfer import FlashInferMetadataBuilder
@@ -53,26 +91,16 @@ def test_flashinfer_workspace_buffer_uses_workspace_manager():
5391 pytest .importorskip ("flashinfer" )
5492 from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
5593
56- FlashInferMetadataBuilder = flashinfer_backend .FlashInferMetadataBuilder
57-
58- def make_builder ():
59- builder = FlashInferMetadataBuilder .__new__ (FlashInferMetadataBuilder )
60- builder ._workspace_buffer = None
61- builder ._workspace_state = flashinfer_backend ._FlashInferWorkspaceState ()
62- builder .device = torch .device ("cpu" )
63- builder .use_dcp = False
64- return builder
65-
6694 reset_workspace_manager ()
6795 init_workspace_manager (torch .device ("cpu" ))
6896 try :
69- first_builder = make_builder ( )
97+ first_builder = _make_flashinfer_builder ( flashinfer_backend )
7098 first_state = first_builder .get_workspace_buffer_state ()
7199 first = first_builder ._get_workspace_buffer (
72100 first_builder ._native_initial_workspace_buffer_size ()
73101 )
74102
75- second_builder = make_builder ( )
103+ second_builder = _make_flashinfer_builder ( flashinfer_backend )
76104 second_builder .set_workspace_buffer_state (first_state )
77105 second = second_builder ._get_workspace_buffer (
78106 second_builder ._native_initial_workspace_buffer_size ()
@@ -90,41 +118,31 @@ def test_flashinfer_workspace_buffer_growth_resets_registered_wrappers():
90118 pytest .importorskip ("flashinfer" )
91119 from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
92120
93- class FakeWrapper :
94- def __init__ (self , float_workspace_buffer ):
95- self ._float_workspace_buffer = float_workspace_buffer
96- self ._int_workspace_buffer = torch .empty (1 , dtype = torch .uint8 )
97- self .reset_calls = 0
98-
99- def reset_workspace_buffer (self , float_workspace_buffer , int_workspace_buffer ):
100- self ._float_workspace_buffer = float_workspace_buffer
101- self ._int_workspace_buffer = int_workspace_buffer
102- self .reset_calls += 1
103-
104- FlashInferMetadataBuilder = flashinfer_backend .FlashInferMetadataBuilder
105- builder = FlashInferMetadataBuilder .__new__ (FlashInferMetadataBuilder )
106- builder ._workspace_buffer = None
107- builder ._workspace_state = flashinfer_backend ._FlashInferWorkspaceState ()
108- builder .device = torch .device ("cpu" )
109- builder .use_dcp = False
121+ WorkspaceSizes = flashinfer_backend .WorkspaceSizes
122+ builder = _make_flashinfer_builder (flashinfer_backend )
110123
111124 reset_workspace_manager ()
112125 init_workspace_manager (torch .device ("cpu" ))
113126 try :
114- wrapper = FakeWrapper (
127+ wrapper = _FakeFlashInferWrapper (
115128 builder ._get_workspace_buffer (
116129 builder ._native_initial_workspace_buffer_size ()
117130 )
118131 )
119132 builder ._register_workspace_wrapper (wrapper )
120- builder ._ensure_flashinfer_wrapper_workspace (wrapper , 1024 )
133+ builder ._ensure_flashinfer_wrapper_workspace (wrapper , WorkspaceSizes ( 1024 , 16 ) )
121134
122135 assert builder ._workspace_buffer .numel () == 1024
123136 assert wrapper ._float_workspace_buffer .data_ptr () == (
124137 builder ._workspace_buffer .data_ptr ()
125138 )
126139 assert wrapper ._float_workspace_buffer .numel () == 1024
127- assert wrapper .reset_calls >= 1
140+ assert wrapper ._int_workspace_buffer .numel () == 16
141+ reset_calls = wrapper .reset_calls
142+ assert reset_calls >= 1
143+
144+ builder ._ensure_flashinfer_wrapper_workspace (wrapper , WorkspaceSizes (1024 , 16 ))
145+ assert wrapper .reset_calls == reset_calls
128146
129147 wrapper_ref = weakref .ref (wrapper )
130148 del wrapper
@@ -137,10 +155,87 @@ def reset_workspace_buffer(self, float_workspace_buffer, int_workspace_buffer):
137155 reset_workspace_manager ()
138156
139157
158+ def test_flashinfer_int_workspace_is_per_wrapper ():
159+ pytest .importorskip ("flashinfer" )
160+ from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
161+
162+ WorkspaceSizes = flashinfer_backend .WorkspaceSizes
163+ builder = _make_flashinfer_builder (flashinfer_backend )
164+
165+ reset_workspace_manager ()
166+ init_workspace_manager (torch .device ("cpu" ))
167+ try :
168+ first = _FakeFlashInferWrapper ()
169+ second = _FakeFlashInferWrapper ()
170+
171+ builder ._ensure_flashinfer_wrapper_workspace (first , WorkspaceSizes (1024 , 32 ))
172+ builder ._ensure_flashinfer_wrapper_workspace (second , WorkspaceSizes (1024 , 32 ))
173+
174+ assert first ._float_workspace_buffer .data_ptr () == (
175+ second ._float_workspace_buffer .data_ptr ()
176+ )
177+ assert first ._int_workspace_buffer .data_ptr () != (
178+ second ._int_workspace_buffer .data_ptr ()
179+ )
180+ assert first ._int_workspace_buffer .numel () == 32
181+ assert second ._int_workspace_buffer .numel () == 32
182+ finally :
183+ reset_workspace_manager ()
184+
185+
186+ def test_flashinfer_finalized_int_workspace_cannot_grow ():
187+ pytest .importorskip ("flashinfer" )
188+ from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
189+
190+ WorkspaceSizes = flashinfer_backend .WorkspaceSizes
191+ builder = _make_flashinfer_builder (flashinfer_backend )
192+ wrapper = _FakeFlashInferWrapper (int_workspace_bytes = 8 )
193+ wrapper ._vllm_flashinfer_int_workspace_finalized = True
194+
195+ reset_workspace_manager ()
196+ init_workspace_manager (torch .device ("cpu" ))
197+ try :
198+ with pytest .raises (AssertionError , match = "int workspace is finalized" ):
199+ builder ._ensure_flashinfer_wrapper_workspace (
200+ wrapper , WorkspaceSizes (1024 , 16 )
201+ )
202+ finally :
203+ reset_workspace_manager ()
204+
205+
206+ def test_flashinfer_non_cudagraph_int_workspace_can_grow (monkeypatch ):
207+ pytest .importorskip ("flashinfer" )
208+ from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
209+
210+ WorkspaceSizes = flashinfer_backend .WorkspaceSizes
211+ builder = _make_flashinfer_builder (flashinfer_backend )
212+ wrapper = _FakeFlashInferWrapper (int_workspace_bytes = 8 )
213+ warnings = []
214+
215+ monkeypatch .setattr (
216+ flashinfer_backend .logger ,
217+ "warning" ,
218+ lambda msg , * args : warnings .append (msg ),
219+ )
220+
221+ reset_workspace_manager ()
222+ init_workspace_manager (torch .device ("cpu" ))
223+ try :
224+ builder ._ensure_flashinfer_wrapper_workspace (wrapper , WorkspaceSizes (1024 , 8 ))
225+ builder ._ensure_flashinfer_wrapper_workspace (wrapper , WorkspaceSizes (1024 , 16 ))
226+
227+ assert wrapper ._int_workspace_buffer .numel () == 16
228+ assert wrapper .reset_calls == 2
229+ assert any ("Growing FlashInfer int workspace" in msg for msg in warnings )
230+ finally :
231+ reset_workspace_manager ()
232+
233+
140234def test_flashinfer_reserves_prefill_tail_workspace (monkeypatch ):
141235 pytest .importorskip ("flashinfer" )
142236 from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
143237
238+ WorkspaceSizes = flashinfer_backend .WorkspaceSizes
144239 FlashInferMetadataBuilder = flashinfer_backend .FlashInferMetadataBuilder
145240 builder = FlashInferMetadataBuilder .__new__ (FlashInferMetadataBuilder )
146241 builder ._workspace_buffer = None
@@ -152,7 +247,8 @@ def test_flashinfer_reserves_prefill_tail_workspace(monkeypatch):
152247 scheduler_config = SimpleNamespace (
153248 max_num_batched_tokens = 8 ,
154249 max_num_seqs = 4 ,
155- )
250+ ),
251+ speculative_config = None ,
156252 )
157253 builder .q_data_type = torch .float16
158254 builder .kv_cache_dtype = torch .uint8
@@ -171,7 +267,7 @@ def fake_workspace_size(**kwargs):
171267 qo_indptr = kwargs ["qo_indptr_cpu" ]
172268 query_lens = torch .diff (qo_indptr ).tolist ()
173269 observed_query_lens .extend (query_lens )
174- return 4096 if query_lens == [3 ] else 0
270+ return WorkspaceSizes ( 4096 , 64 ) if query_lens == [3 ] else WorkspaceSizes ( 0 , 0 )
175271
176272 monkeypatch .setattr (
177273 builder ,
@@ -187,19 +283,63 @@ def fake_workspace_size(**kwargs):
187283 "_ensure_flashinfer_wrapper_workspace" ,
188284 lambda wrapper , size : ensured .append (size ),
189285 )
286+ monkeypatch .setattr (
287+ builder ,
288+ "_reserve_decode_wrapper_workspace" ,
289+ lambda ** kwargs : WorkspaceSizes (0 , 0 ),
290+ )
291+ builder .enable_cuda_graph = False
190292
191293 reset_workspace_manager ()
192294 init_workspace_manager (torch .device ("cpu" ))
193295 try :
194- assert builder .reserve_workspace_for_cudagraph_capture () == 4096
296+ assert builder .reserve_workspace_for_cudagraph_capture () == 4160
195297 finally :
196298 reset_workspace_manager ()
197299
198- assert ensured == [4096 ]
300+ assert ensured == [WorkspaceSizes ( 4096 , 64 ) ]
199301 assert 3 in observed_query_lens
200302 assert 8 in observed_query_lens
201303
202304
305+ def test_flashinfer_reserves_decode_cudagraph_int_workspace (monkeypatch ):
306+ pytest .importorskip ("flashinfer" )
307+ from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
308+
309+ WorkspaceSizes = flashinfer_backend .WorkspaceSizes
310+ builder = _make_flashinfer_builder (flashinfer_backend )
311+ builder .decode_fixed_split_size = - 1
312+ builder .disable_split_kv = False
313+
314+ wrapper = _FakeFlashInferWrapper ()
315+ wrapper .is_cuda_graph_enabled = True
316+
317+ monkeypatch .setattr (builder , "_get_decode_wrapper" , lambda * args : wrapper )
318+ monkeypatch .setattr (
319+ builder ,
320+ "_get_decode_workspace_size" ,
321+ lambda ** kwargs : WorkspaceSizes (128 , 32 ),
322+ )
323+
324+ reset_workspace_manager ()
325+ init_workspace_manager (torch .device ("cpu" ))
326+ try :
327+ sizes = builder ._reserve_decode_wrapper_workspace (
328+ batch_size = 4 ,
329+ num_pages = 8 ,
330+ last_page_len = 16 ,
331+ use_cudagraph = True ,
332+ )
333+ finally :
334+ reset_workspace_manager ()
335+
336+ assert sizes == WorkspaceSizes (128 , 32 )
337+ assert wrapper ._float_workspace_buffer .numel () == 128
338+ assert wrapper ._int_workspace_buffer .numel () == 32
339+ assert wrapper .reset_calls == 1
340+ assert wrapper ._vllm_flashinfer_int_workspace_finalized
341+
342+
203343def test_flashinfer_workspace_query_len_candidates ():
204344 pytest .importorskip ("flashinfer" )
205345 from vllm .v1 .attention .backends import flashinfer as flashinfer_backend
0 commit comments