@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 92183b41f3
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 15, 06:00 UTC

[Bugfix][Core] Prefix caching causes incorrect outputs due to outdated ComputedBlocksTracker (#18957)

Signed-off-by: 刘全 <quan.liu2@dbappsecurity.com.cn> Co-authored-by: 刘全 <quan.liu2@dbappsecurity.com.cn>

quanliu committed Jun 16, 2025 at 12:56 UTC 92183b41f3b13c0d8fd0da4361713f7c3cbbbe56
3 files changed +331
tests/core/test_scheduler.py
+294
@@ -1041,3 +1041,297 @@ def test_no_batches_mixed_with_prompt_tokens_and_prompt_embeds():
1041 for seq in scheduled_seq_group.seq_group.seqs:
1042 seq.status = SequenceStatus.FINISHED_STOPPED
1043 scheduler.free_finished_seq_groups()
1044 +
1045 +
1046 +def test_remove_seq_from_computed_blocks_tracker():
1047 + """
1048 + Test that computed_blocks_tracker correctly removes stale sequences
1049 + during scheduling.
1050 +
1051 + The test covers 9 scheduling branches where stale seqs are removed:
1052 + - 1 in _schedule_swapped
1053 + - 1 in _schedule_priority_preemption
1054 + - 7 in _schedule_prefill
1055 +
1056 + Each branch is tested to ensure proper cleanup of
1057 + _seq_id_to_num_tokens_computed.
1058 + """
1059 + # Budget can not schedule in swapped
1060 + block_size = 2
1061 + max_seq_group = 3
1062 + seq_tokens_with_swapped: list[list[int]] = []
1063 + blocks_to_swap_out: list[tuple[int, int]] = []
1064 + curr_loras: set[int] = set()
1065 +
1066 + scheduler = initialize_scheduler(
1067 + block_size=block_size,
1068 + num_cpu_blocks=64,
1069 + num_gpu_blocks=16,
1070 + max_num_seqs=max_seq_group,
1071 + enable_prefix_caching=True,
1072 + )
1073 + budget = create_token_budget(token_budget=15)
1074 +
1075 + seq_length = 16
1076 + num_seqs = 3
1077 + for i in range(num_seqs):
1078 + seq_tokens_with_swapped.append([i] * seq_length)
1079 +
1080 + seq_and_seq_groups = [
1081 + create_dummy_prompt(f"{i}",
1082 + prompt_tokens=seq_tokens_with_swapped[i],
1083 + block_size=block_size)
1084 + for i in range(len(seq_tokens_with_swapped))
1085 + ]
1086 +
1087 + for _, seq_group in seq_and_seq_groups:
1088 + scheduler._allocate_and_set_running(seq_group)
1089 + scheduler._swap_out(seq_group, blocks_to_swap_out)
1090 + scheduler._add_seq_group_to_swapped(seq_group)
1091 +
1092 + scheduler._schedule_swapped(budget, curr_loras)
1093 + seq_id_to_num_tokens_computed = (
1094 + scheduler.block_manager._computed_blocks_tracker.
1095 + _seq_id_to_num_tokens_computed.get(1))
1096 + assert seq_id_to_num_tokens_computed is None
1097 +
1098 + # Prefill schedule don't have a space for another LoRA, so
1099 + # we ignore this request for now.
1100 + block_size = 4
1101 + lora_config = LoRAConfig(max_lora_rank=8, max_loras=1)
1102 + scheduler = initialize_scheduler(lora_config=lora_config,
1103 + block_size=block_size,
1104 + num_cpu_blocks=64,
1105 + num_gpu_blocks=64,
1106 + enable_prefix_caching=True)
1107 + budget = create_token_budget(token_budget=120)
1108 + num_seqs = 2
1109 + for i in range(num_seqs):
1110 + _, seq_group = create_dummy_prompt(str(i),
1111 + prompt_length=seq_length,
1112 + block_size=block_size,
1113 + lora_request=LoRARequest(
1114 + lora_name=str(i),
1115 + lora_int_id=i + 1,
1116 + lora_path="abc"))
1117 + scheduler.add_seq_group(seq_group)
1118 +
1119 + scheduler._schedule_prefills(budget, curr_loras)
1120 + seq_id_to_num_tokens_computed = (
1121 + scheduler.block_manager._computed_blocks_tracker.
1122 + _seq_id_to_num_tokens_computed.get(1))
1123 + assert seq_id_to_num_tokens_computed is None
1124 +
1125 + # Priority preemption schedule
1126 + scheduler._schedule_priority_preemption(budget)
1127 + seq_id_to_num_tokens_computed = (
1128 + scheduler.block_manager._computed_blocks_tracker.
1129 + _seq_id_to_num_tokens_computed.get(1))
1130 + assert seq_id_to_num_tokens_computed is None
1131 +
1132 + # Prefill scheduler does not schedule batches with prompt tokens and
1133 + # prompt embeddings co-mingled.
1134 + block_size = 2
1135 + max_seq_group = 3
1136 + scheduler = initialize_scheduler(
1137 + block_size=block_size,
1138 + num_cpu_blocks=16,
1139 + num_gpu_blocks=16,
1140 + max_num_seqs=max_seq_group,
1141 + max_model_len=100,
1142 + enable_prefix_caching=True,
1143 + )
1144 + seq_length = 7
1145 + embedding_size = 5
1146 + seq_tokens_with_embedding: list[list[int]] = []
1147 + seq_embeds: list[Optional[torch.Tensor]] = []
1148 +
1149 + seq_tokens_with_embedding.append(list(range(seq_length)))
1150 + seq_embeds.append(None)
1151 + seq_tokens_with_embedding.append([0] * seq_length)
1152 + seq_embeds.append(torch.rand(embedding_size))
1153 +
1154 + seq_and_seq_groups = [
1155 + create_dummy_prompt(f"{i}",
1156 + prompt_tokens=seq_tokens_with_embedding[i],
1157 + prompt_embeds=seq_embeds[i],
1158 + block_size=block_size)
1159 + for i in range(len(seq_tokens_with_embedding))
1160 + ]
1161 +
1162 + for _, seq_group in seq_and_seq_groups:
1163 + scheduler.add_seq_group(seq_group)
1164 +
1165 + scheduler._schedule_default()
1166 + seq_id_to_num_tokens_computed = (
1167 + scheduler.block_manager._computed_blocks_tracker.
1168 + _seq_id_to_num_tokens_computed.get(1))
1169 + assert seq_id_to_num_tokens_computed is None
1170 +
1171 + # Prefill scheduler budget num_batched_tokens
1172 + # >= scheduler_config max_num_batched_tokens
1173 + block_size = 2
1174 + max_seq_group = 3
1175 + seq_tokens_prefill_budget: list[list[int]] = []
1176 +
1177 + scheduler = initialize_scheduler(
1178 + block_size=block_size,
1179 + max_token_budget=8,
1180 + num_cpu_blocks=16,
1181 + num_gpu_blocks=16,
1182 + max_num_seqs=max_seq_group,
1183 + max_model_len=5,
1184 + enable_prefix_caching=True,
1185 + )
1186 + seq_length = 4
1187 + num_seqs = 3
1188 + for i in range(num_seqs):
1189 + seq_tokens_prefill_budget.append([i] * seq_length)
1190 +
1191 + seq_and_seq_groups = [
1192 + create_dummy_prompt(f"{i}",
1193 + prompt_tokens=seq_tokens_prefill_budget[i],
1194 + block_size=block_size)
1195 + for i in range(len(seq_tokens_prefill_budget))
1196 + ]
1197 +
1198 + for _, seq_group in seq_and_seq_groups:
1199 + scheduler.add_seq_group(seq_group)
1200 +
1201 + scheduler._schedule_default()
1202 + seq_id_to_num_tokens_computed = (
1203 + scheduler.block_manager._computed_blocks_tracker.
1204 + _seq_id_to_num_tokens_computed.get(2))
1205 + assert seq_id_to_num_tokens_computed is None
1206 +
1207 + # Budget can not schedule in waiting
1208 + block_size = 2
1209 + max_seq_group = 3
1210 +
1211 + scheduler = initialize_scheduler(
1212 + block_size=block_size,
1213 + max_token_budget=30,
1214 + num_cpu_blocks=16,
1215 + num_gpu_blocks=16,
1216 + max_num_seqs=max_seq_group,
1217 + max_model_len=30,
1218 + enable_prefix_caching=True,
1219 + )
1220 + seq_length = 16
1221 + num_seqs = 3
1222 + seq_tokens_prefill_budget_waiting: list[list[int]] = []
1223 +
1224 + for i in range(num_seqs):
1225 + seq_tokens_prefill_budget_waiting.append(list(range(seq_length)))
1226 +
1227 + seq_and_seq_groups = [
1228 + create_dummy_prompt(f"{i}",
1229 + prompt_tokens=seq_tokens_prefill_budget_waiting[i],
1230 + block_size=block_size)
1231 + for i in range(len(seq_tokens_prefill_budget_waiting))
1232 + ]
1233 +
1234 + for _, seq_group in seq_and_seq_groups:
1235 + scheduler.add_seq_group(seq_group)
1236 +
1237 + scheduler._schedule_default()
1238 + seq_id_to_num_tokens_computed = (
1239 + scheduler.block_manager._computed_blocks_tracker.
1240 + _seq_id_to_num_tokens_computed.get(1))
1241 + assert seq_id_to_num_tokens_computed is None
1242 +
1243 + # Sequence num_new_tokens > prompt_limit marked FINISHED_IGNORED
1244 + block_size = 2
1245 + max_seq_group = 3
1246 + scheduler = initialize_scheduler(
1247 + block_size=block_size,
1248 + num_cpu_blocks=16,
1249 + num_gpu_blocks=16,
1250 + max_num_seqs=max_seq_group,
1251 + max_model_len=30,
1252 + enable_prefix_caching=True,
1253 + )
1254 +
1255 + seq_length = 31
1256 + seq_tokens_prompt_limit: list[list[int]] = []
1257 + seq_tokens_prompt_limit.append(list(range(seq_length)))
1258 + seq_and_seq_groups = [
1259 + create_dummy_prompt("0",
1260 + prompt_tokens=seq_tokens_prompt_limit[0],
1261 + block_size=block_size)
1262 + ]
1263 + for _, seq_group in seq_and_seq_groups:
1264 + scheduler.add_seq_group(seq_group)
1265 + scheduler._schedule_default()
1266 + seq_id_to_num_tokens_computed = (
1267 + scheduler.block_manager._computed_blocks_tracker.
1268 + _seq_id_to_num_tokens_computed.get(0))
1269 + assert seq_id_to_num_tokens_computed is None
1270 +
1271 + # Budget can not allocate, AllocStatus is NEVER marked FINISHED_IGNORED
1272 + block_size = 2
1273 + max_seq_group = 3
1274 + scheduler = initialize_scheduler(
1275 + block_size=block_size,
1276 + num_cpu_blocks=160,
1277 + num_gpu_blocks=160,
1278 + max_num_seqs=max_seq_group,
1279 + max_model_len=320,
1280 + enable_prefix_caching=True,
1281 + )
1282 +
1283 + seq_length = 320
1284 + num_seqs = 1
1285 + seq_tokens_never: list[list[int]] = []
1286 + for i in range(num_seqs):
1287 + seq_tokens_never.append(list(range(seq_length)))
1288 +
1289 + seq_and_seq_groups = [
1290 + create_dummy_prompt(f"{i}",
1291 + prompt_tokens=seq_tokens_never[i],
1292 + block_size=block_size)
1293 + for i in range(len(seq_tokens_never))
1294 + ]
1295 +
1296 + for _, seq_group in seq_and_seq_groups:
1297 + scheduler.add_seq_group(seq_group)
1298 +
1299 + scheduler._schedule_default()
1300 + seq_id_to_num_tokens_computed = (
1301 + scheduler.block_manager._computed_blocks_tracker.
1302 + _seq_id_to_num_tokens_computed.get(0))
1303 + assert seq_id_to_num_tokens_computed is None
1304 +
1305 + # Budget can not allocate, AllocStatus is LATER
1306 + block_size = 2
1307 + max_seq_group = 3
1308 + scheduler = initialize_scheduler(
1309 + block_size=block_size,
1310 + num_cpu_blocks=160,
1311 + num_gpu_blocks=160,
1312 + max_num_seqs=max_seq_group,
1313 + max_model_len=320,
1314 + enable_prefix_caching=True,
1315 + )
1316 +
1317 + seq_length = 160
1318 + num_seqs = 2
1319 + seq_tokens_later: list[list[int]] = []
1320 + for i in range(num_seqs):
1321 + seq_tokens_later.append(list(range(seq_length)))
1322 +
1323 + seq_and_seq_groups = [
1324 + create_dummy_prompt(f"{i}",
1325 + prompt_tokens=seq_tokens_later[i],
1326 + block_size=block_size)
1327 + for i in range(len(seq_tokens_later))
1328 + ]
1329 +
1330 + for _, seq_group in seq_and_seq_groups:
1331 + scheduler.add_seq_group(seq_group)
1332 +
1333 + scheduler._schedule_default()
1334 + seq_id_to_num_tokens_computed = (
1335 + scheduler.block_manager._computed_blocks_tracker.
1336 + _seq_id_to_num_tokens_computed.get(1))
1337 + assert seq_id_to_num_tokens_computed is None
vllm/core/block_manager.py
+4
@@ -270,6 +270,10 @@ class SelfAttnBlockSpaceManager(BlockSpaceManager):
270 self.block_tables[seq_id].free()
271 del self.block_tables[seq_id]
272
273 + def remove_seq_from_computed_blocks_tracker(self, seq: Sequence) -> None:
274 + seq_id = seq.seq_id
275 + self._computed_blocks_tracker.remove_seq(seq_id)
276 +
277 def free_cross(self, seq_group: SequenceGroup) -> None:
278 request_id = seq_group.request_id
279 if request_id not in self.cross_block_tables:
vllm/core/scheduler.py
+33
@@ -901,6 +901,8 @@ class Scheduler:
901 num_new_tokens=num_new_tokens_uncached,
902 num_new_seqs=num_new_seqs,
903 ):
904 + self.remove_seq_from_computed_blocks_tracker(
905 + seq_group, SequenceStatus.SWAPPED)
906 break
907
908 if lora_int_id > 0 and curr_loras is not None:
@@ -1024,6 +1026,9 @@ class Scheduler:
1026 # Put the sequence back into the waiting queue
1027 waiting_queue.appendleft(seq_group)
1028
1029 + self.remove_seq_from_computed_blocks_tracker(
1030 + seq_group, SequenceStatus.WAITING)
1031 +
1032 waiting_queue = deque(sorted(waiting_queue, key=self._get_priority))
1033
1034 self.waiting = waiting_queue
@@ -1113,6 +1118,8 @@ class Scheduler:
1118 )
1119 for seq in waiting_seqs:
1120 seq.status = SequenceStatus.FINISHED_IGNORED
1121 + self.remove_seq_from_computed_blocks_tracker(
1122 + seq_group, SequenceStatus.FINISHED_IGNORED)
1123 ignored_seq_groups.append(seq_group)
1124 waiting_queue.popleft()
1125 continue
@@ -1126,6 +1133,8 @@ class Scheduler:
1133 can_allocate = self.block_manager.can_allocate(
1134 seq_group, num_lookahead_slots=num_lookahead_slots)
1135 if can_allocate == AllocStatus.LATER:
1136 + self.remove_seq_from_computed_blocks_tracker(
1137 + seq_group, SequenceStatus.WAITING)
1138 break
1139 elif can_allocate == AllocStatus.NEVER:
1140 logger.warning(
@@ -1136,6 +1145,8 @@ class Scheduler:
1145 )
1146 for seq in waiting_seqs:
1147 seq.status = SequenceStatus.FINISHED_IGNORED
1148 + self.remove_seq_from_computed_blocks_tracker(
1149 + seq_group, SequenceStatus.FINISHED_IGNORED)
1150 ignored_seq_groups.append(seq_group)
1151 waiting_queue.popleft()
1152 continue
@@ -1145,6 +1156,8 @@ class Scheduler:
1156 if len(seq_groups) == 0:
1157 using_prompt_embeds = seq_group.uses_prompt_embeds()
1158 if using_prompt_embeds != seq_group.uses_prompt_embeds():
1159 + self.remove_seq_from_computed_blocks_tracker(
1160 + seq_group, SequenceStatus.WAITING)
1161 leftover_waiting_sequences.appendleft(seq_group)
1162 waiting_queue.popleft()
1163 continue
@@ -1159,6 +1172,8 @@ class Scheduler:
1172 and len(curr_loras) >= self.lora_config.max_loras):
1173 # We don't have a space for another LoRA, so
1174 # we ignore this request for now.
1175 + self.remove_seq_from_computed_blocks_tracker(
1176 + seq_group, SequenceStatus.WAITING)
1177 leftover_waiting_sequences.appendleft(seq_group)
1178 waiting_queue.popleft()
1179 continue
@@ -1168,6 +1183,8 @@ class Scheduler:
1183 # We've reached the budget limit - since there might be
1184 # continuous prefills in the running queue, we should break
1185 # to avoid scheduling any new prefills.
1186 + self.remove_seq_from_computed_blocks_tracker(
1187 + seq_group, SequenceStatus.WAITING)
1188 break
1189
1190 num_new_seqs = seq_group.get_max_num_running_seqs()
@@ -1175,6 +1192,8 @@ class Scheduler:
1192 num_new_tokens=num_new_tokens_uncached,
1193 num_new_seqs=num_new_seqs,
1194 ):
1195 + self.remove_seq_from_computed_blocks_tracker(
1196 + seq_group, SequenceStatus.WAITING)
1197 break
1198
1199 # Can schedule this request.
@@ -1688,6 +1707,20 @@ class Scheduler:
1707 """Free a sequence from a block table."""
1708 self.block_manager.free(seq)
1709
1710 + def remove_seq_from_computed_blocks_tracker(
1711 + self, seq_group: SequenceGroup,
1712 + status: Optional[SequenceStatus]) -> None:
1713 + seqs = seq_group.get_seqs(status=status)
1714 + for seq in seqs:
1715 + self._remove_seq_from_computed_blocks_tracker(seq)
1716 +
1717 + def _remove_seq_from_computed_blocks_tracker(self, seq: Sequence) -> None:
1718 + """
1719 + Free a sequence computed blocks tracker _seq_id_to_blocks_hashes
1720 + and _seq_id_to_num_tokens_computed.
1721 + """
1722 + self.block_manager.remove_seq_from_computed_blocks_tracker(seq)
1723 +
1724 def _free_finished_seqs(self, seq_group: SequenceGroup) -> None:
1725 """Free finished seqs in a sequence group."""
1726 for seq in seq_group.get_seqs():