@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / e1d85e5c24
Fork 0

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 6, 07:00 UTC

[Attention] Support distinguishing between short extends and decodes (#37303)

Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>

Lucas Wilkinson committed Mar 20, 2026 at 10:49 UTC e1d85e5c2454bd8d349dbe676679380cbe0e920a
9 files changed +178 -135
.buildkite/test_areas/engine.yaml
+12
@@ -70,3 +70,15 @@ steps:
70 device: mi325_4
71 depends_on:
72 - image-build-amd
73 +
74 +- label: V1 e2e (4xH100)
75 + timeout_in_minutes: 60
76 + device: h100
77 + num_devices: 4
78 + optional: true
79 + source_file_dependencies:
80 + - vllm/v1/attention/backends/utils.py
81 + - vllm/v1/worker/gpu_model_runner.py
82 + - tests/v1/e2e/test_hybrid_chunked_prefill.py
83 + commands:
84 + - pytest -v -s v1/e2e/test_hybrid_chunked_prefill.py
tests/v1/attention/test_batch_reordering.py
+86 -37
@@ -10,9 +10,10 @@ from vllm.v1.attention.backends.utils import reorder_batch_to_split_decodes_and_
10
11
12 class MockInputBatch:
13 - def __init__(self, req_ids, num_computed_tokens_cpu):
13 + def __init__(self, req_ids, num_computed_tokens_cpu, num_prompt_tokens):
14 self.req_ids = req_ids
15 self.num_computed_tokens_cpu = num_computed_tokens_cpu
16 + self.num_prompt_tokens = num_prompt_tokens
17
18 def swap_states(self, i, j):
19 self.req_ids[i], self.req_ids[j] = self.req_ids[j], self.req_ids[i]
@@ -20,6 +21,10 @@ class MockInputBatch:
21 self.num_computed_tokens_cpu[j],
22 self.num_computed_tokens_cpu[i],
23 )
24 + self.num_prompt_tokens[i], self.num_prompt_tokens[j] = (
25 + self.num_prompt_tokens[j],
26 + self.num_prompt_tokens[i],
27 + )
28
29
30 class MockSchedulerOutput:
@@ -29,96 +34,139 @@ class MockSchedulerOutput:
34
35 @dataclass
36 class ReorderTestCase:
32 - requests: list[tuple[int, int]] # (num_scheduled_tokens, num_computed_tokens)
37 + # (num_scheduled_tokens, num_computed_tokens, num_prompt_tokens)
38 + requests: list[tuple[int, int, int]]
39 expected_order: list[int]
40 expected_modified: bool
41 decode_threshold: int = 1
42
43
44 # Test cases for batch reordering
45 +# Format: (num_scheduled, num_computed, num_prompt)
46 REORDER_TEST_CASES = {
47 "all_decodes": ReorderTestCase(
41 - requests=[(1, 10), (1, 20), (1, 30)],
48 + requests=[(1, 10, 10), (1, 20, 20), (1, 30, 30)],
49 expected_order=[0, 1, 2],
50 expected_modified=False,
51 ),
45 - "all_prefills": ReorderTestCase(
46 - requests=[(100, 100), (200, 200), (300, 300)],
52 + "all_long_extends": ReorderTestCase(
53 + requests=[(100, 100, 100), (200, 200, 200), (300, 300, 300)],
54 expected_order=[0, 1, 2],
55 expected_modified=False,
56 ),
50 - "mixed_interleaved": ReorderTestCase(
51 - requests=[(100, 100), (1, 10), (200, 200), (1, 20)],
52 - expected_order=[3, 1, 2, 0], # Only swap 0↔3, keep 1 and 2 in place
57 + "mixed_decodes_long_extends": ReorderTestCase(
58 + requests=[(100, 100, 100), (1, 10, 10), (200, 200, 200), (1, 20, 20)],
59 + expected_order=[3, 1, 2, 0],
60 expected_modified=True,
61 ),
62 "already_ordered": ReorderTestCase(
56 - requests=[(1, 10), (1, 20), (100, 100), (200, 0)],
63 + requests=[(1, 10, 10), (1, 20, 20), (100, 100, 100), (200, 0, 200)],
64 expected_order=[0, 1, 2, 3],
65 expected_modified=False,
66 ),
67 "single_request": ReorderTestCase(
61 - requests=[(1, 10)],
68 + requests=[(1, 10, 10)],
69 expected_order=[0],
70 expected_modified=False,
71 ),
72 "higher_threshold": ReorderTestCase(
66 - requests=[(2, 10), (3, 20), (5, 30), (6, 40)],
73 + requests=[(2, 10, 10), (3, 20, 20), (5, 30, 30), (6, 40, 40)],
74 expected_order=[0, 1, 2, 3],
75 expected_modified=False,
76 decode_threshold=4,
77 ),
78 "decodes_at_end": ReorderTestCase(
72 - requests=[(100, 100), (200, 200), (1, 10), (1, 20)],
79 + requests=[(100, 100, 100), (200, 200, 200), (1, 10, 10), (1, 20, 20)],
80 expected_order=[2, 3, 0, 1],
81 expected_modified=True,
82 ),
76 - "decode_extend_prefill": ReorderTestCase(
77 - requests=[(100, 0), (10, 50), (1, 10)],
83 + "decode_long_extend_prefill": ReorderTestCase(
84 + requests=[(100, 0, 100), (10, 50, 50), (1, 10, 10)],
85 expected_order=[2, 1, 0],
86 expected_modified=True,
87 ),
81 - "extend_prefill_only": ReorderTestCase(
82 - requests=[(100, 0), (10, 50), (200, 0), (20, 75)],
83 - expected_order=[3, 1, 2, 0], # Only swap 0↔3, keep 1 and 2 in place
88 + "long_extend_prefill_only": ReorderTestCase(
89 + requests=[(100, 0, 100), (10, 50, 50), (200, 0, 200), (20, 75, 75)],
90 + expected_order=[3, 1, 2, 0],
91 expected_modified=True,
92 ),
86 - "complicated_mixed_interleaved": ReorderTestCase(
93 + "complicated_mixed": ReorderTestCase(
94 requests=[
88 - (1, 20),
89 - (1, 50),
90 - (374, 0),
91 - (300, 20),
92 - (1, 20),
93 - (256, 0),
94 - (1, 5),
95 - (27, 0),
96 - (1, 4),
95 + (1, 20, 20), # decode
96 + (1, 50, 50), # decode
97 + (374, 0, 374), # prefill
98 + (300, 20, 20), # long_extend
99 + (1, 20, 20), # decode
100 + (256, 0, 256), # prefill
101 + (1, 5, 5), # decode
102 + (27, 0, 27), # prefill
103 + (1, 4, 4), # decode
104 ],
105 expected_order=[0, 1, 6, 8, 4, 3, 2, 7, 5],
106 expected_modified=True,
107 ),
108 "new_request_single_token_prefill": ReorderTestCase(
109 requests=[
103 - (100, 0),
104 - (1, 0), # New request with only 1 token (STILL prefill)
105 - (50, 100),
106 - (1, 10),
110 + (100, 0, 100), # prefill
111 + (1, 0, 1), # prefill (single token, still prefill)
112 + (50, 100, 100), # long_extend
113 + (1, 10, 10), # decode
114 ],
108 - # Only index 3 is a true decode (has num_computed_tokens > 0)
115 expected_order=[3, 2, 0, 1],
116 expected_modified=True,
117 ),
118 "multiple_new_requests_single_token_prefill": ReorderTestCase(
119 requests=[
114 - (1, 0), # New prefill (1 token, no computed)
115 - (1, 0), # New prefill (1 token, no computed)
116 - (1, 50),
117 - (200, 0),
120 + (1, 0, 1), # prefill
121 + (1, 0, 1), # prefill
122 + (1, 50, 50), # decode
123 + (200, 0, 200), # prefill
124 ],
125 expected_order=[2, 1, 0, 3],
126 expected_modified=True,
127 ),
128 + "four_way_already_ordered": ReorderTestCase(
129 + requests=[
130 + (1, 100, 100), # decode
131 + (1, 50, 100), # short_extend
132 + (10, 50, 100), # long_extend
133 + (100, 0, 100), # prefill
134 + ],
135 + expected_order=[0, 1, 2, 3],
136 + expected_modified=False,
137 + ),
138 + "four_way_needs_reorder": ReorderTestCase(
139 + requests=[
140 + (100, 0, 100), # prefill
141 + (1, 50, 100), # short_extend
142 + (1, 100, 100), # decode
143 + (10, 50, 100), # long_extend
144 + ],
145 + expected_order=[2, 1, 3, 0],
146 + expected_modified=True,
147 + ),
148 + "four_way_multiple_short_extends": ReorderTestCase(
149 + requests=[
150 + (2, 100, 100), # decode
151 + (2, 50, 200), # short_extend
152 + (2, 75, 150), # short_extend
153 + (2, 200, 200), # decode
154 + ],
155 + expected_order=[0, 3, 2, 1],
156 + expected_modified=True,
157 + decode_threshold=2,
158 + ),
159 + "four_way_spec_decode_threshold": ReorderTestCase(
160 + requests=[
161 + (5, 100, 100), # decode
162 + (5, 50, 100), # short_extend
163 + (5, 0, 100), # prefill
164 + (10, 50, 100), # long_extend
165 + ],
166 + expected_order=[0, 1, 3, 2],
167 + expected_modified=True,
168 + decode_threshold=5,
169 + ),
170 }
171
172
@@ -129,8 +177,9 @@ def test_reorder_batch_to_split_decodes_and_prefills(test_case: ReorderTestCase)
177 req_ids = [f"r{i}" for i in range(len(test_case.requests))]
178 num_computed_tokens = np.array([r[1] for r in test_case.requests], dtype=np.int32)
179 num_scheduled_tokens = {f"r{i}": r[0] for i, r in enumerate(test_case.requests)}
180 + num_prompt_tokens = np.array([r[2] for r in test_case.requests], dtype=np.int32)
181
133 - input_batch = MockInputBatch(req_ids, num_computed_tokens)
182 + input_batch = MockInputBatch(req_ids, num_computed_tokens, num_prompt_tokens)
183 scheduler_output = MockSchedulerOutput(num_scheduled_tokens)
184
185 modified = reorder_batch_to_split_decodes_and_prefills(
tests/v1/e2e/test_hybrid_chunked_prefill.py
+2 -2
@@ -43,7 +43,7 @@ MESSAGES = [
43 pytest.param("Qwen/Qwen3.5-4B", marks=[large_gpu_mark(min_gb=40)]),
44 pytest.param(
45 "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8",
46 - marks=[large_gpu_mark(min_gb=80)] + multi_gpu_marks(num_gpus=2),
46 + marks=[large_gpu_mark(min_gb=80)] + multi_gpu_marks(num_gpus=4),
47 ),
48 ],
49 )
@@ -68,7 +68,7 @@ def test_mtp_speculative_mixed_batch_short_prefill(
68 max_num_batched_tokens=chunk_size,
69 max_model_len=512,
70 enforce_eager=True,
71 - tensor_parallel_size=2,
71 + tensor_parallel_size=4,
72 trust_remote_code=True,
73 enable_chunked_prefill=True,
74 enable_prefix_caching=enable_prefix_caching,
vllm/v1/attention/backend.py
+6
@@ -362,6 +362,11 @@ class CommonAttentionMetadata:
362 dcp_local_seq_lens_cpu: torch.Tensor | None = None
363 """Sequence lengths of the local rank in decode context parallelism world"""
364
365 + is_prefilling: torch.Tensor | None = None
366 + """(batch_size,) bool tensor: True if request is still in prefill phase
367 + (num_computed_tokens < num_prompt_tokens). Used by some backends to
368 + distinguish actual decodes from short extends."""
369 +
370 # WARNING: Deprecated fields. Will be removed in a future release (v0.15.0)
371 _seq_lens_cpu: torch.Tensor | None = None
372 _num_computed_tokens_cpu: torch.Tensor | None = None
@@ -443,6 +448,7 @@ class CommonAttentionMetadata:
448 encoder_seq_lens_cpu=maybe_slice_reqs(self.encoder_seq_lens_cpu),
449 dcp_local_seq_lens=maybe_slice_reqs(self.dcp_local_seq_lens),
450 dcp_local_seq_lens_cpu=maybe_slice_reqs(self.dcp_local_seq_lens_cpu),
451 + is_prefilling=maybe_slice_reqs(self.is_prefilling),
452 )
453
454
vllm/v1/attention/backends/mamba_attn.py
+3 -1
@@ -358,7 +358,9 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
358
359 num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
360 split_decodes_and_prefills(
361 - common_attn_metadata, decode_threshold=decode_threshold
361 + common_attn_metadata,
362 + decode_threshold=decode_threshold,
363 + treat_short_extends_as_decodes=False,
364 )
365 )
366
vllm/v1/attention/backends/utils.py
+49 -24
@@ -489,11 +489,15 @@ def split_decodes_and_prefills(
489 common_attn_metadata: CommonAttentionMetadata,
490 decode_threshold: int = 1,
491 require_uniform: bool = False,
492 + treat_short_extends_as_decodes: bool = True,
493 ) -> tuple[int, int, int, int]:
494 """
495 Assuming a reordered batch, finds the boundary between prefill and decode
496 requests.
497
498 + The batch is expected to be ordered as:
499 + decode → short_extend → long_extend → prefill
500 +
501 Args:
502 common_attn_metadata: CommonAttentionMetadata object containing the
503 batch metadata.
@@ -501,6 +505,9 @@ def split_decodes_and_prefills(
505 require_uniform: If True, requires that all decode requests have the
506 same query length. When set, some queries may be considered prefills
507 even if they are <= decode_threshold, in order to ensure uniformity.
508 + treat_short_extends_as_decodes: If True (default), short extends
509 + (query_len <= threshold but still prefilling) are counted as
510 + decodes. If False, they are counted as prefills.
511
512 Returns:
513 num_decodes: The number of decode requests.
@@ -513,8 +520,10 @@ def split_decodes_and_prefills(
520 num_tokens = common_attn_metadata.num_actual_tokens
521 query_start_loc = common_attn_metadata.query_start_loc_cpu
522
516 - if max_query_len <= decode_threshold and (
517 - not require_uniform or decode_threshold <= 1
523 + if (
524 + max_query_len <= decode_threshold
525 + and (not require_uniform or decode_threshold <= 1)
526 + and treat_short_extends_as_decodes
527 ):
528 return num_reqs, 0, num_tokens, 0
529
@@ -533,11 +542,14 @@ def split_decodes_and_prefills(
542 else:
543 is_prefill = query_lens > decode_threshold
544
545 + if not treat_short_extends_as_decodes:
546 + assert common_attn_metadata.is_prefilling is not None
547 + is_prefill |= common_attn_metadata.is_prefilling
548 +
549 if not torch.any(is_prefill):
550 return num_reqs, 0, num_tokens, 0
551
552 first_prefill = is_prefill.int().argmax(dim=-1).item()
540 - assert torch.all(query_lens[:first_prefill] <= decode_threshold)
553 num_decodes = first_prefill
554 num_prefills = num_reqs - num_decodes
555 num_decode_tokens = query_start_loc[first_prefill].item()
@@ -581,39 +593,52 @@ def reorder_batch_to_split_decodes_and_prefills(
593 Reorders the batch to split into prefill and decode requests; places all
594 requests with <= decode_threshold tokens at the front of the batch.
595
596 + The batch is reordered into 4 regions:
597 + decode: (num_scheduled <= threshold AND is not prefilling)
598 + short_extend: (num_scheduled <= threshold AND is chunked prefilling)
599 + long_extend: (num_scheduled > threshold AND is chunked prefilling)
600 + prefill: (num_computed == 0) # First chunks
601 +
602 Returns:
603 True if the batch was modified, False otherwise.
604 """
587 - # We now want to reorder the batch into decode → extend → prefill order
588 - # where:
589 - # decode: request with num_scheduled_tokens <= decode_threshold
590 - # extend: non-decode request with existing context
591 - # prefill: non-decode request with no existing context
592 - # NOTE for now we loosely use "decode" to mean requests where attention is
593 - # likely memory-bound and "prefill" to mean requests where attention is
594 - # likely compute-bound,
605 num_reqs = len(input_batch.req_ids)
606 num_scheduled_tokens = [
607 scheduler_output.num_scheduled_tokens[id] for id in input_batch.req_ids
608 ]
609 num_scheduled_tokens_np = np.array(num_scheduled_tokens)
610 num_computed_tokens_np = input_batch.num_computed_tokens_cpu[:num_reqs]
601 -
602 - is_prefill = num_computed_tokens_np == 0
603 - is_decode = (num_scheduled_tokens_np <= decode_threshold) & (~is_prefill)
604 - is_extend = (num_scheduled_tokens_np > decode_threshold) & (~is_prefill)
605 -
606 - # Desired order: decode → extend → prefill
607 - req_regions = np.zeros(is_decode.shape, dtype=np.int32) # 0 = decode by default
608 - req_regions[is_extend] = 1
609 - req_regions[is_prefill] = 2
611 + num_prompt_tokens_np = input_batch.num_prompt_tokens[:num_reqs]
612 +
613 + has_context = num_computed_tokens_np > 0
614 + is_below_threshold = num_scheduled_tokens_np <= decode_threshold
615 + done_prefilling = num_computed_tokens_np >= num_prompt_tokens_np
616 +
617 + # Mutually exclusive categories (exactly one True per request):
618 + # 1. No context yet -> prefill
619 + # 2. Has context, above threshold -> long_extend
620 + # 3. Has context, below threshold, still prefilling -> short_extend
621 + # 4. Has context, below threshold, done prefilling -> decode
622 + is_pure_prefill = ~has_context
623 + is_long_extend = has_context & ~is_below_threshold
624 + is_short_extend = has_context & is_below_threshold & ~done_prefilling
625 + is_decode = has_context & is_below_threshold & done_prefilling
626 +
627 + # Desired order: decode → short_extend → long_extend → prefill
628 + req_regions = np.zeros(num_reqs, dtype=np.int32) # 0 = decode by default
629 + req_regions[is_short_extend] = 1
630 + req_regions[is_long_extend] = 2
631 + req_regions[is_pure_prefill] = 3
632
633 num_decodes = int(is_decode.sum())
612 - num_extends = int(is_extend.sum())
634 + num_short_extends = int(is_short_extend.sum())
635 + num_long_extends = int(is_long_extend.sum())
636 + num_prefills = int(is_pure_prefill.sum())
637
614 - target_regions = np.zeros(num_reqs, dtype=np.int32)
615 - target_regions[num_decodes : num_decodes + num_extends] = 1
616 - target_regions[num_decodes + num_extends :] = 2
638 + target_regions = np.repeat(
639 + [0, 1, 2, 3],
640 + [num_decodes, num_short_extends, num_long_extends, num_prefills],
641 + ).astype(np.int32)
642
643 needs_swap = req_regions != target_regions
644
vllm/v1/worker/gpu_input_batch.py
+7 -1
@@ -134,7 +134,13 @@ class InputBatch:
134 pin_memory=pin_memory,
135 )
136 self.num_tokens_no_spec = self.num_tokens_no_spec_cpu_tensor.numpy()
137 - self.num_prompt_tokens = np.zeros(max_num_reqs, dtype=np.int32)
137 + self.num_prompt_tokens_cpu_tensor = torch.zeros(
138 + (max_num_reqs,),
139 + device="cpu",
140 + dtype=torch.int32,
141 + pin_memory=pin_memory,
142 + )
143 + self.num_prompt_tokens = self.num_prompt_tokens_cpu_tensor.numpy()
144 self.num_computed_tokens_cpu_tensor = torch.zeros(
145 (max_num_reqs,),
146 device="cpu",
vllm/v1/worker/gpu_model_runner.py
+13 -28
@@ -740,19 +740,6 @@ class GPUModelRunner(
740
741 self.uniform_decode_query_len = 1 + self.num_spec_tokens
742
743 - # When spec decode is active, the mamba backend classifies requests
744 - # with query_len <= reorder_batch_threshold as "decodes". Prefill
745 - # chunks that fall under this threshold get processed via the decode
746 - # path, which stores intermediate states at sequential slots. We must
747 - # set num_accepted_tokens to the chunk's query_len for those requests
748 - # so the next iteration reads from the correct final-state slot.
749 - # Prefills that went through the actual prefill path should keep the
750 - # default value of 1 (the prefill path stores state at slot 0 only).
751 - self.needs_prefill_as_decode_slots: bool = False
752 - self.prefill_as_decode_num_tokens = self._make_buffer(
753 - self.max_num_reqs, dtype=torch.int32
754 - )
755 -
743 # Cudagraph dispatcher for runtime cudagraph dispatching.
744 self.cudagraph_dispatcher = CudagraphDispatcher(self.vllm_config)
745
@@ -1369,16 +1356,6 @@ class GPUModelRunner(
1356 .int()
1357 .argmax(-1)
1358 )
1372 - spec_decode_active = bool(scheduler_output.scheduled_spec_decode_tokens)
1373 - if self.needs_prefill_as_decode_slots and spec_decode_active:
1374 - mamba_utils.update_accepted_tokens_for_prefill_as_decode(
1375 - self.input_batch,
1376 - self.prefill_as_decode_num_tokens,
1377 - self.num_accepted_tokens.gpu,
1378 - scheduler_output,
1379 - self.reorder_batch_threshold,
1380 - num_reqs,
1381 - )
1359
1360 if self.cache_config.mamba_cache_mode == "align":
1361 for i, num_tokens in enumerate(
@@ -1982,14 +1959,23 @@ class GPUModelRunner(
1959 attn_gid = self.routed_experts_attn_gid
1960 slot_mapping_attn = slot_mappings[attn_gid]
1961 self.slot_mapping = slot_mapping_attn[:num_tokens].cpu().numpy()
1962 + # Compute is_prefilling: True if request is still in prefill phase
1963 + # (num_computed_tokens < num_prompt_tokens). Used by mamba backends to
1964 + # distinguish actual decodes from short extends.
1965 + num_computed_tokens_cpu = self.input_batch.num_computed_tokens_cpu_tensor[
1966 + :num_reqs_padded
1967 + ]
1968 + num_prompt_tokens_cpu = self.input_batch.num_prompt_tokens_cpu_tensor[
1969 + :num_reqs_padded
1970 + ]
1971 + is_prefilling = num_computed_tokens_cpu < num_prompt_tokens_cpu
1972 +
1973 cm_base = CommonAttentionMetadata(
1974 query_start_loc=self.query_start_loc.gpu[: num_reqs_padded + 1],
1975 query_start_loc_cpu=self.query_start_loc.cpu[: num_reqs_padded + 1],
1976 seq_lens=self.seq_lens.gpu[:num_reqs_padded],
1977 _seq_lens_cpu=self.seq_lens.cpu[:num_reqs_padded],
1990 - _num_computed_tokens_cpu=self.input_batch.num_computed_tokens_cpu_tensor[
1991 - :num_reqs_padded
1992 - ],
1978 + _num_computed_tokens_cpu=num_computed_tokens_cpu,
1979 num_reqs=num_reqs_padded,
1980 num_actual_tokens=num_tokens_padded,
1981 max_query_len=max_query_len,
@@ -1997,6 +1983,7 @@ class GPUModelRunner(
1983 block_table_tensor=block_table_gid_0,
1984 slot_mapping=slot_mapping_gid_0,
1985 causal=True,
1986 + is_prefilling=is_prefilling,
1987 )
1988
1989 if self.dcp_world_size > 1:
@@ -2048,8 +2035,6 @@ class GPUModelRunner(
2035 else 0
2036 )
2037
2051 - if isinstance(builder, Mamba2AttentionMetadataBuilder):
2052 - self.needs_prefill_as_decode_slots = True
2038 extra_attn_metadata_args = {}
2039 if use_spec_decode and isinstance(
2040 builder, (Mamba2AttentionMetadataBuilder, GDNAttentionMetadataBuilder)
vllm/v1/worker/mamba_utils.py
-42
@@ -266,45 +266,3 @@ def postprocess_mamba(
266 if src_block_idx == dest_block_idx:
267 num_accepted_tokens_cpu[i] = 1
268 do_mamba_copy_block(copy_bufs)
269 -
270 -
271 -def update_accepted_tokens_for_prefill_as_decode(
272 - input_batch: GPUInputBatch,
273 - prefill_as_decode_num_tokens: CpuGpuBuffer,
274 - num_accepted_tokens_gpu: torch.Tensor,
275 - scheduler_output: SchedulerOutput,
276 - decode_qlen_threshold: int | None,
277 - num_reqs: int,
278 -):
279 - """
280 - Adjusts num_accepted_tokens for prefill chunks processed via the decode path.
281 - This ensures subsequent iterations read from the correct sequential state slot
282 - instead of the default prefill slot 0. Not used by GDN attention, which manually
283 - separates short prefills and short decodes when building the attention metadata.
284 - """
285 - any_is_prefill = False
286 - for i in range(num_reqs):
287 - num_computed = input_batch.num_computed_tokens_cpu[i]
288 - num_prompt = input_batch.num_prompt_tokens[i]
289 - is_prefill = num_computed < num_prompt
290 - req_id = input_batch.req_ids[i]
291 - query_len = scheduler_output.num_scheduled_tokens[req_id]
292 -
293 - if is_prefill:
294 - classified_as_decode = (
295 - decode_qlen_threshold is not None and query_len <= decode_qlen_threshold
296 - )
297 - num_tokens = query_len if classified_as_decode else 1
298 - any_is_prefill = True
299 - else:
300 - num_tokens = -1
301 - prefill_as_decode_num_tokens.np[i] = num_tokens
302 -
303 - # We can skip the GPU transfer if there aren't any values to update
304 - if any_is_prefill:
305 - prefill_as_decode_num_tokens.copy_to_gpu(num_reqs)
306 - num_accepted_tokens_gpu[:num_reqs] = torch.where(
307 - prefill_as_decode_num_tokens.gpu[:num_reqs] != -1,
308 - prefill_as_decode_num_tokens.gpu[:num_reqs],
309 - num_accepted_tokens_gpu[:num_reqs],
310 - )