A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 2, 11:00 UTC
[Misc] added unit tests for the core pooling methods (#43818)
Signed-off-by: Taneem Ibrahim <taneem.ibrahim@gmail.com> Co-authored-by: Wentao Ye <44945378+yewentao256@users.noreply.github.com>
Taneem Ibrahim committed
May 29, 2026 at 10:40 UTC
5502c3b52d0b1c8c6cd71fe938212f95e399450e
1 file changed
+499
tests/model_executor/layers/test_pooler_methods.py
new
+499
@@ -0,0 +1,499 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Unit tests for sequence and token pooling methods and their factories."""
4
+
5
+from dataclasses import dataclass
6
+from unittest.mock import patch
7
+
8
+import pytest
9
+import torch
10
+
11
+from vllm.model_executor.layers.pooler.seqwise.methods import (
12
+ CLSPool,
13
+ LastPool,
14
+ MeanPool,
15
+ get_seq_pooling_method,
16
+)
17
+from vllm.model_executor.layers.pooler.tokwise.methods import (
18
+ AllPool,
19
+ StepPool,
20
+ get_tok_pooling_method,
21
+)
22
+from vllm.pooling_params import PoolingParams
23
+from vllm.v1.pool.metadata import PoolingCursor, PoolingMetadata, PoolingStates
24
+
25
+_CPU = torch.device("cpu")
26
+
27
+
28
+def _make_pooling_cursor(
29
+ prompt_lens: list[int],
30
+ *,
31
+ num_scheduled_tokens: list[int] | None = None,
32
+ seq_lens: list[int] | None = None,
33
+ device: torch.device = _CPU,
34
+) -> PoolingCursor:
35
+ """Build a PoolingCursor from a list of per-sequence prompt lengths."""
36
+ prompt_lens_cpu = torch.tensor(prompt_lens, dtype=torch.long)
37
+ if num_scheduled_tokens is None:
38
+ num_scheduled_tokens_cpu = prompt_lens_cpu.clone()
39
+ else:
40
+ num_scheduled_tokens_cpu = torch.tensor(num_scheduled_tokens, dtype=torch.long)
41
+ if seq_lens is None:
42
+ seq_lens_cpu = prompt_lens_cpu.clone()
43
+ else:
44
+ seq_lens_cpu = torch.tensor(seq_lens, dtype=torch.long)
45
+
46
+ cumsum = torch.zeros(len(prompt_lens) + 1, dtype=torch.long, device=device)
47
+ torch.cumsum(num_scheduled_tokens_cpu, dim=0, out=cumsum[1:])
48
+
49
+ return PoolingCursor(
50
+ first_token_indices_gpu=cumsum[: len(prompt_lens)].to(device),
51
+ last_token_indices_gpu=(cumsum[1:] - 1).to(device),
52
+ prompt_lens_cpu=prompt_lens_cpu,
53
+ seq_lens_cpu=seq_lens_cpu,
54
+ num_scheduled_tokens_cpu=num_scheduled_tokens_cpu,
55
+ )
56
+
57
+
58
+def _make_metadata(
59
+ prompt_lens: list[int],
60
+ *,
61
+ tasks: list[str] | None = None,
62
+ token_ids: list[list[int]] | None = None,
63
+ pooling_params: list[PoolingParams] | None = None,
64
+ num_scheduled_tokens: list[int] | None = None,
65
+ seq_lens: list[int] | None = None,
66
+ device: torch.device = _CPU,
67
+) -> PoolingMetadata:
68
+ """Build a minimal PoolingMetadata for testing pooling methods."""
69
+ n_seqs = len(prompt_lens)
70
+ if tasks is None:
71
+ tasks = ["embed"] * n_seqs
72
+ if pooling_params is None:
73
+ pooling_params = [PoolingParams(task=t) for t in tasks]
74
+
75
+ prompt_lens_tensor = torch.tensor(prompt_lens, dtype=torch.long)
76
+
77
+ prompt_token_ids_cpu = None
78
+ prompt_token_ids = None
79
+ if token_ids is not None:
80
+ max_len = max(len(t) for t in token_ids)
81
+ padded = [t + [0] * (max_len - len(t)) for t in token_ids]
82
+ prompt_token_ids_cpu = torch.tensor(padded, dtype=torch.long)
83
+ prompt_token_ids = prompt_token_ids_cpu.to(device)
84
+
85
+ cursor = _make_pooling_cursor(
86
+ prompt_lens,
87
+ num_scheduled_tokens=num_scheduled_tokens,
88
+ seq_lens=seq_lens,
89
+ device=device,
90
+ )
91
+
92
+ pooling_states = [PoolingStates() for _ in range(n_seqs)]
93
+
94
+ return PoolingMetadata(
95
+ prompt_lens=prompt_lens_tensor,
96
+ prompt_token_ids=prompt_token_ids,
97
+ prompt_token_ids_cpu=prompt_token_ids_cpu,
98
+ pooling_params=pooling_params,
99
+ pooling_states=pooling_states,
100
+ pooling_cursor=cursor,
101
+ )
102
+
103
+
104
+# ---------------------------------------------------------------------------
105
+# CLSPool
106
+# ---------------------------------------------------------------------------
107
+class TestCLSPool:
108
+ def test_extracts_first_token(self):
109
+ hidden = torch.tensor(
110
+ [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0]]
111
+ )
112
+ metadata = _make_metadata([2, 3])
113
+ pooler = CLSPool()
114
+ out = pooler(hidden, metadata)
115
+ expected = torch.tensor([[1.0, 2.0], [5.0, 6.0]])
116
+ assert torch.equal(out, expected)
117
+
118
+ def test_rejects_partial_prefill(self):
119
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
120
+ metadata = _make_metadata([3], num_scheduled_tokens=[2])
121
+ pooler = CLSPool()
122
+ with pytest.raises(AssertionError, match="partial prefill"):
123
+ pooler(hidden, metadata)
124
+
125
+
126
+# ---------------------------------------------------------------------------
127
+# LastPool
128
+# ---------------------------------------------------------------------------
129
+class TestLastPool:
130
+ def test_extracts_last_token(self):
131
+ hidden = torch.tensor(
132
+ [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0]]
133
+ )
134
+ metadata = _make_metadata([2, 3])
135
+ pooler = LastPool()
136
+ out = pooler(hidden, metadata)
137
+ expected = torch.tensor([[3.0, 4.0], [9.0, 10.0]])
138
+ assert torch.equal(out, expected)
139
+
140
+ def test_partial_prefill_extracts_last_scheduled(self):
141
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
142
+ metadata = _make_metadata([4], num_scheduled_tokens=[2])
143
+ pooler = LastPool()
144
+ out = pooler(hidden, metadata)
145
+ expected = torch.tensor([[3.0, 4.0]])
146
+ assert torch.equal(out, expected)
147
+
148
+
149
+# ---------------------------------------------------------------------------
150
+# MeanPool
151
+# ---------------------------------------------------------------------------
152
+class TestMeanPool:
153
+ def test_computes_mean(self):
154
+ hidden = torch.tensor(
155
+ [[1.0, 2.0], [3.0, 4.0], [10.0, 20.0]], dtype=torch.float32
156
+ )
157
+ metadata = _make_metadata([2, 1])
158
+ pooler = MeanPool()
159
+ out = pooler(hidden, metadata)
160
+ expected = torch.tensor([[2.0, 3.0], [10.0, 20.0]], dtype=torch.float32)
161
+ assert torch.allclose(out, expected, atol=1e-5)
162
+
163
+ def test_single_token_is_identity(self):
164
+ hidden = torch.tensor([[5.0, 10.0]], dtype=torch.float32)
165
+ metadata = _make_metadata([1])
166
+ pooler = MeanPool()
167
+ out = pooler(hidden, metadata)
168
+ assert torch.allclose(out, hidden, atol=1e-5)
169
+
170
+ def test_uniform_values_return_same(self):
171
+ hidden = torch.full((4, 3), 7.0, dtype=torch.float32)
172
+ metadata = _make_metadata([4])
173
+ pooler = MeanPool()
174
+ out = pooler(hidden, metadata)
175
+ expected = torch.full((1, 3), 7.0, dtype=torch.float32)
176
+ assert torch.allclose(out, expected, atol=1e-5)
177
+
178
+ def test_multiple_sequences(self):
179
+ hidden = torch.tensor(
180
+ [
181
+ [0.0, 0.0],
182
+ [2.0, 4.0],
183
+ [4.0, 8.0],
184
+ [10.0, 10.0],
185
+ ],
186
+ dtype=torch.float32,
187
+ )
188
+ metadata = _make_metadata([3, 1])
189
+ pooler = MeanPool()
190
+ out = pooler(hidden, metadata)
191
+ expected = torch.tensor([[2.0, 4.0], [10.0, 10.0]], dtype=torch.float32)
192
+ assert torch.allclose(out, expected, atol=1e-5)
193
+
194
+ def test_empty_batch(self):
195
+ hidden = torch.empty((0, 8), dtype=torch.float32)
196
+ metadata = _make_metadata([])
197
+ pooler = MeanPool()
198
+ out = pooler(hidden, metadata)
199
+ assert out.shape == (0, 8)
200
+
201
+ def test_rejects_partial_prefill(self):
202
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32)
203
+ metadata = _make_metadata([3], num_scheduled_tokens=[2])
204
+ pooler = MeanPool()
205
+ with pytest.raises(AssertionError, match="partial prefill"):
206
+ pooler(hidden, metadata)
207
+
208
+ def test_chunked_accumulation(self):
209
+ hidden = torch.arange(20, dtype=torch.float32).reshape(5, 4)
210
+ metadata = _make_metadata([3, 2])
211
+ pooler = MeanPool()
212
+ with patch(
213
+ "vllm.model_executor.layers.pooler.seqwise.methods"
214
+ "._MEAN_POOL_ACCUMULATION_CHUNK_BYTES",
215
+ 16,
216
+ ):
217
+ out = pooler(hidden, metadata)
218
+ expected_seq0 = hidden[:3].float().mean(dim=0, keepdim=True)
219
+ expected_seq1 = hidden[3:].float().mean(dim=0, keepdim=True)
220
+ expected = torch.cat([expected_seq0, expected_seq1], dim=0)
221
+ assert torch.allclose(out, expected, atol=1e-5)
222
+
223
+ def test_upcasts_to_float32(self):
224
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float16)
225
+ metadata = _make_metadata([2])
226
+ pooler = MeanPool()
227
+ out = pooler(hidden, metadata)
228
+ assert out.dtype == torch.float32
229
+ expected = torch.tensor([[2.0, 3.0]], dtype=torch.float32)
230
+ assert torch.allclose(out, expected, atol=1e-2)
231
+
232
+
233
+# ---------------------------------------------------------------------------
234
+# get_seq_pooling_method factory
235
+# ---------------------------------------------------------------------------
236
+class TestGetSeqPoolingMethod:
237
+ def test_cls(self):
238
+ assert isinstance(get_seq_pooling_method("CLS"), CLSPool)
239
+
240
+ def test_last(self):
241
+ assert isinstance(get_seq_pooling_method("LAST"), LastPool)
242
+
243
+ def test_mean(self):
244
+ assert isinstance(get_seq_pooling_method("MEAN"), MeanPool)
245
+
246
+ def test_unknown_raises(self):
247
+ with pytest.raises(NotImplementedError, match="UNKNOWN"):
248
+ get_seq_pooling_method("UNKNOWN")
249
+
250
+
251
+# ---------------------------------------------------------------------------
252
+# AllPool
253
+# ---------------------------------------------------------------------------
254
+
255
+
256
+@dataclass
257
+class _FakeSchedulerConfig:
258
+ enable_chunked_prefill: bool = False
259
+
260
+
261
+@dataclass
262
+class _FakeVllmConfig:
263
+ scheduler_config: _FakeSchedulerConfig
264
+
265
+
266
+class TestAllPool:
267
+ @staticmethod
268
+ def _make_all_pool(*, chunked: bool = False) -> AllPool:
269
+ fake_config = _FakeVllmConfig(
270
+ scheduler_config=_FakeSchedulerConfig(
271
+ enable_chunked_prefill=chunked,
272
+ ),
273
+ )
274
+ with patch(
275
+ "vllm.model_executor.layers.pooler.tokwise.methods.get_current_vllm_config",
276
+ return_value=fake_config,
277
+ ):
278
+ return AllPool()
279
+
280
+ def test_splits_by_sequence(self):
281
+ pooler = self._make_all_pool()
282
+ hidden = torch.tensor(
283
+ [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0]]
284
+ )
285
+ metadata = _make_metadata([2, 3])
286
+ out = pooler(hidden, metadata)
287
+ assert len(out) == 2
288
+ assert torch.equal(out[0], hidden[:2])
289
+ assert torch.equal(out[1], hidden[2:])
290
+
291
+ def test_single_sequence(self):
292
+ pooler = self._make_all_pool()
293
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
294
+ metadata = _make_metadata([3])
295
+ out = pooler(hidden, metadata)
296
+ assert len(out) == 1
297
+ assert torch.equal(out[0], hidden)
298
+
299
+ def test_chunked_prefill_returns_none_for_unfinished(self):
300
+ pooler = self._make_all_pool(chunked=True)
301
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
302
+ metadata = _make_metadata(
303
+ [4],
304
+ num_scheduled_tokens=[2],
305
+ seq_lens=[2],
306
+ )
307
+ out = pooler(hidden, metadata)
308
+ assert len(out) == 1
309
+ assert out[0] is None
310
+
311
+ def test_chunked_prefill_returns_concat_when_finished(self):
312
+ pooler = self._make_all_pool(chunked=True)
313
+
314
+ chunk1 = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
315
+ metadata1 = _make_metadata(
316
+ [4],
317
+ num_scheduled_tokens=[2],
318
+ seq_lens=[2],
319
+ )
320
+ out1 = pooler(chunk1, metadata1)
321
+ assert out1[0] is None
322
+
323
+ chunk2 = torch.tensor([[5.0, 6.0], [7.0, 8.0]])
324
+ metadata2 = _make_metadata(
325
+ [4],
326
+ num_scheduled_tokens=[2],
327
+ seq_lens=[4],
328
+ )
329
+ metadata2.pooling_states = metadata1.pooling_states
330
+ out2 = pooler(chunk2, metadata2)
331
+ assert out2[0] is not None
332
+ expected = torch.cat([chunk1, chunk2], dim=0)
333
+ assert torch.equal(out2[0], expected)
334
+
335
+ def test_chunked_prefill_single_shot_matches_non_chunked(self):
336
+ pooler = self._make_all_pool(chunked=True)
337
+ hidden = torch.tensor(
338
+ [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0]]
339
+ )
340
+ metadata = _make_metadata([2, 3])
341
+ out = pooler(hidden, metadata)
342
+ assert len(out) == 2
343
+ assert torch.equal(out[0], hidden[:2])
344
+ assert torch.equal(out[1], hidden[2:])
345
+
346
+ def test_chunked_prefill_mixed_finished_unfinished(self):
347
+ pooler = self._make_all_pool(chunked=True)
348
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
349
+ metadata = _make_metadata(
350
+ [2, 4],
351
+ num_scheduled_tokens=[2, 1],
352
+ seq_lens=[2, 1],
353
+ )
354
+ out = pooler(hidden, metadata)
355
+ assert len(out) == 2
356
+ assert torch.equal(out[0], hidden[:2])
357
+ assert out[1] is None
358
+
359
+
360
+# ---------------------------------------------------------------------------
361
+# StepPool
362
+# ---------------------------------------------------------------------------
363
+class TestStepPool:
364
+ @staticmethod
365
+ def _make_step_pool(*, chunked: bool = False) -> StepPool:
366
+ fake_config = _FakeVllmConfig(
367
+ scheduler_config=_FakeSchedulerConfig(
368
+ enable_chunked_prefill=chunked,
369
+ ),
370
+ )
371
+ with patch(
372
+ "vllm.model_executor.layers.pooler.tokwise.methods.get_current_vllm_config",
373
+ return_value=fake_config,
374
+ ):
375
+ return StepPool()
376
+
377
+ def test_filters_by_step_tag_id(self):
378
+ pooler = self._make_step_pool()
379
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]])
380
+ token_ids = [[10, 99, 10, 20]]
381
+ params = [PoolingParams(task="token_classify", step_tag_id=10)]
382
+ metadata = _make_metadata([4], token_ids=token_ids, pooling_params=params)
383
+ out = pooler(hidden, metadata)
384
+ assert len(out) == 1
385
+ expected = torch.tensor([[1.0, 2.0], [5.0, 6.0]])
386
+ assert torch.equal(out[0], expected)
387
+
388
+ def test_filters_by_returned_token_ids(self):
389
+ pooler = self._make_step_pool()
390
+ hidden = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
391
+ token_ids = [[10, 20]]
392
+ params = [PoolingParams(task="token_classify", returned_token_ids=[0, 2])]
393
+ metadata = _make_metadata([2], token_ids=token_ids, pooling_params=params)
394
+ out = pooler(hidden, metadata)
395
+ assert len(out) == 1
396
+ expected = torch.tensor([[1.0, 3.0], [4.0, 6.0]])
397
+ assert torch.equal(out[0], expected)
398
+
399
+ def test_no_filtering_without_params(self):
400
+ pooler = self._make_step_pool()
401
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
402
+ token_ids = [[10, 20]]
403
+ params = [PoolingParams(task="token_classify")]
404
+ metadata = _make_metadata([2], token_ids=token_ids, pooling_params=params)
405
+ out = pooler(hidden, metadata)
406
+ assert len(out) == 1
407
+ assert torch.equal(out[0], hidden)
408
+
409
+ def test_combined_step_tag_and_returned_token_ids(self):
410
+ pooler = self._make_step_pool()
411
+ hidden = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
412
+ token_ids = [[99, 10, 99]]
413
+ params = [
414
+ PoolingParams(
415
+ task="token_classify",
416
+ step_tag_id=10,
417
+ returned_token_ids=[0, 2],
418
+ )
419
+ ]
420
+ metadata = _make_metadata([3], token_ids=token_ids, pooling_params=params)
421
+ out = pooler(hidden, metadata)
422
+ assert len(out) == 1
423
+ expected = torch.tensor([[4.0, 6.0]])
424
+ assert torch.equal(out[0], expected)
425
+
426
+ def test_step_tag_id_no_match_returns_empty(self):
427
+ pooler = self._make_step_pool()
428
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
429
+ token_ids = [[10, 20]]
430
+ params = [PoolingParams(task="token_classify", step_tag_id=999)]
431
+ metadata = _make_metadata([2], token_ids=token_ids, pooling_params=params)
432
+ out = pooler(hidden, metadata)
433
+ assert len(out) == 1
434
+ assert out[0].shape == (0, 2)
435
+
436
+ def test_chunked_prefill_propagates_none_for_unfinished(self):
437
+ pooler = self._make_step_pool(chunked=True)
438
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
439
+ token_ids = [[10, 20, 30, 40]]
440
+ params = [PoolingParams(task="token_classify", step_tag_id=10)]
441
+ metadata = _make_metadata(
442
+ [4],
443
+ token_ids=token_ids,
444
+ pooling_params=params,
445
+ num_scheduled_tokens=[2],
446
+ seq_lens=[2],
447
+ )
448
+ out = pooler(hidden, metadata)
449
+ assert len(out) == 1
450
+ assert out[0] is None
451
+
452
+ def test_chunked_prefill_filters_when_finished(self):
453
+ pooler = self._make_step_pool(chunked=True)
454
+ hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]])
455
+ token_ids = [[10, 99, 10, 20]]
456
+ params = [PoolingParams(task="token_classify", step_tag_id=10)]
457
+ metadata = _make_metadata([4], token_ids=token_ids, pooling_params=params)
458
+ out = pooler(hidden, metadata)
459
+ assert len(out) == 1
460
+ expected = torch.tensor([[1.0, 2.0], [5.0, 6.0]])
461
+ assert torch.equal(out[0], expected)
462
+
463
+ def test_requires_token_ids_update(self):
464
+ pooler = self._make_step_pool()
465
+ update = pooler.get_pooling_updates("token_classify")
466
+ assert update.requires_token_ids is True
467
+
468
+
469
+# ---------------------------------------------------------------------------
470
+# get_tok_pooling_method factory
471
+# ---------------------------------------------------------------------------
472
+class TestGetTokPoolingMethod:
473
+ def test_all(self):
474
+ fake_config = _FakeVllmConfig(
475
+ scheduler_config=_FakeSchedulerConfig(
476
+ enable_chunked_prefill=False,
477
+ ),
478
+ )
479
+ with patch(
480
+ "vllm.model_executor.layers.pooler.tokwise.methods.get_current_vllm_config",
481
+ return_value=fake_config,
482
+ ):
483
+ assert isinstance(get_tok_pooling_method("ALL"), AllPool)
484
+
485
+ def test_step(self):
486
+ fake_config = _FakeVllmConfig(
487
+ scheduler_config=_FakeSchedulerConfig(
488
+ enable_chunked_prefill=False,
489
+ ),
490
+ )
491
+ with patch(
492
+ "vllm.model_executor.layers.pooler.tokwise.methods.get_current_vllm_config",
493
+ return_value=fake_config,
494
+ ):
495
+ assert isinstance(get_tok_pooling_method("STEP"), StepPool)
496
+
497
+ def test_unknown_raises(self):
498
+ with pytest.raises(NotImplementedError, match="UNKNOWN"):
499
+ get_tok_pooling_method("UNKNOWN")