Skip to content

Commit 478c9fa

Browse files
authored
[RL] pause: use abort pipeline with scheduling loop alive for graceful termination (#7806)
Replace the old preempted_all + error_response approach in _control_pause with a two-phase design: Phase 1: Block new requests via _rejecting_new_requests (NOT is_paused) - Scheduling loop keeps running so _trigger_abort can process - add_abort_req_ids(ALL) marks all requests for abort - Scheduling loop catches them via _trigger_abort as they cycle through Phase 2: After drain, set is_paused=True to fully stop scheduling loop - Handle scheduler-only stragglers with direct _send_error_response - Wait for output queue empty, then reset Depends-on: #7615 (refact abort_requests to fire-and-forget)
1 parent dc1fea1 commit 478c9fa

2 files changed

Lines changed: 41 additions & 36 deletions

File tree

fastdeploy/engine/common_engine.py

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def __init__(self, cfg: FDConfig, start_queue=True, use_async_llm=False):
139139

140140
self.is_paused = False # pause request generation
141141
self._pause_cond = threading.Condition()
142+
self._rejecting_new_requests = False # blocks new requests during abort drain
142143

143144
self._ctrl_output_queues = {}
144145
self._ctrl_response_mailboxes = collections.defaultdict(collections.OrderedDict)
@@ -1106,7 +1107,7 @@ def _insert_zmq_task_to_scheduler(self):
11061107
trace_print(LoggingEventName.REQUEST_QUEUE_START, data["request_id"], data.get("user", ""))
11071108
self.llm_logger.debug(f"Receive request from api server: {request}")
11081109

1109-
if self.is_paused:
1110+
if self.is_paused or self._rejecting_new_requests:
11101111
self.llm_logger.warning(f"Engine is paused, drop request: {request}")
11111112
self._send_error_response(
11121113
request.request_id,
@@ -1226,39 +1227,20 @@ def _control_pause(self, control_request: ControlRequest):
12261227
if self.is_paused:
12271228
self.llm_logger.info("Engine is already paused, no need to pause again.")
12281229
return
1229-
self.is_paused = True
1230+
self._rejecting_new_requests = True
1231+
self.resource_manager.log_status()
12301232

1231-
self.llm_logger.info("Abort running requests.")
1233+
# Scheduling loop picks them up via _trigger_abort when they enter resource_manager
1234+
all_req_ids = list(set(self.resource_manager.requests.keys()) | set(self.scheduler.requests.keys()))
1235+
self.llm_logger.info(f"Pause: aborting {len(all_req_ids)} total requests.")
1236+
if all_req_ids:
1237+
self.resource_manager.add_abort_req_ids(all_req_ids)
1238+
self._wait_inflight_drained()
12321239

1233-
self.resource_manager.log_status()
1234-
# preempted all running reqs. preempted reqs will be append to ResourceManager.waiting queue
1235-
timeout, count = 60, 0
1236-
while self.engine_worker_queue.exist_tasks():
1237-
time.sleep(0.001)
1238-
count += 1
1239-
if count >= timeout * 1000:
1240-
break
1241-
if count >= timeout * 1000:
1242-
error_msg = f"Emptying engine worker queue timed out after {timeout} seconds, worker may hanged!"
1243-
self.llm_logger.error(error_msg)
1244-
raise Exception(error_msg)
1245-
running_reqs = self.resource_manager.preempted_all()
1246-
if len(running_reqs) > 0:
1247-
self.llm_logger.info(f"Total {len(running_reqs)} requests need to be aborted.")
1248-
self.resource_manager.get_real_bsz()
1249-
self.engine_worker_queue.put_tasks((running_reqs, self.resource_manager.real_bsz))
1250-
self.resource_manager.wait_worker_inflight_requests_finish(timeout=60)
1251-
# self.engine_worker_queue.clear_data()
1252-
self.token_processor.clear_data()
1240+
with self._pause_cond:
1241+
self.is_paused = True
12531242
self.resource_manager.log_status()
12541243

1255-
# abort inflight requests to user
1256-
inflight_requests = self.scheduler.get_inflight_requests()
1257-
self.llm_logger.info(f"Abort inflight requests (total {len(inflight_requests)}).")
1258-
for req in inflight_requests:
1259-
self._send_error_response(req.request_id, "Request is aborted since engine is paused.")
1260-
self.scheduler.reset()
1261-
12621244
# pause cache transfer
12631245
if self.cfg.cache_config.num_cpu_blocks > 0 or self.cfg.cache_config.kvcache_storage_backend:
12641246
self.llm_logger.info("Start to pause cache transfer.")
@@ -1278,6 +1260,21 @@ def _control_pause(self, control_request: ControlRequest):
12781260
self.llm_logger.info("Successfully paused request generation.")
12791261
return None
12801262

1263+
def _wait_inflight_drained(self):
1264+
"""
1265+
Wait until resource_manager.requests is completely empty.
1266+
No timeout — abort pipeline will complete. Aligned with SGLang's poll-until-drained.
1267+
"""
1268+
start_time = time.time()
1269+
while (
1270+
self.resource_manager.requests
1271+
or self.scheduler.requests
1272+
or self.resource_manager.waiting_abort_req_id_set
1273+
or self.resource_manager.to_be_aborted_req_id_set
1274+
):
1275+
time.sleep(0.005)
1276+
self.llm_logger.info(f"All inflight requests drained, take time: {time.time() - start_time:.3f} seconds")
1277+
12811278
def _control_resume(self, control_request: ControlRequest) -> Optional[dict]:
12821279
"""Control function for resuming request generation.
12831280
@@ -1293,6 +1290,7 @@ def _control_resume(self, control_request: ControlRequest) -> Optional[dict]:
12931290
self.llm_logger.info("Engine is not paused, no need to resume.")
12941291
return None
12951292
self.is_paused = False
1293+
self._rejecting_new_requests = False
12961294
self._pause_cond.notify_all()
12971295

12981296
# resume cache transfer

tests/engine/test_common_engine.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,22 +1139,29 @@ def test_control_pause_and_resume_paths(self):
11391139
eng = self._make_mixed_engine()
11401140
eng.is_paused = False
11411141
eng._pause_cond = threading.Condition()
1142-
eng.engine_worker_queue = Mock(exist_tasks=Mock(return_value=False), put_tasks=Mock())
1142+
eng.engine_worker_queue = Mock(exist_tasks=Mock(return_value=False))
11431143
eng.resource_manager = Mock(
1144-
preempted_all=Mock(return_value=[Request(request_id="r1", prompt_token_ids=[1], prompt_token_ids_len=1)]),
1145-
get_real_bsz=Mock(),
1146-
wait_worker_inflight_requests_finish=Mock(),
1144+
requests={"r1": Mock(output_token_ids=[1, 2, 3])},
1145+
waiting_abort_req_id_set=set(),
1146+
to_be_aborted_req_id_set=set(),
1147+
add_abort_req_ids=Mock(),
11471148
log_status=Mock(),
11481149
cache_manager=Mock(reset=Mock()),
1149-
real_bsz=1,
11501150
)
11511151
eng.token_processor = Mock(clear_data=Mock())
1152-
eng.scheduler = Mock(get_inflight_requests=Mock(return_value=[]), reset=Mock())
1152+
mock_scheduler = Mock(reset=Mock())
1153+
mock_scheduler.requests = {}
1154+
mock_scheduler.mutex = threading.Lock()
1155+
mock_scheduler.responses = {}
1156+
mock_scheduler.batch_responses_per_step = []
1157+
eng.scheduler = mock_scheduler
11531158
eng._send_error_response = Mock()
1159+
eng._wait_inflight_drained = Mock()
11541160

11551161
with patch("fastdeploy.engine.common_engine.envs.ENABLE_V1_KVCACHE_SCHEDULER", True):
11561162
eng._control_pause(ControlRequest(request_id="ctrl1", method="pause"))
11571163
self.assertTrue(eng.is_paused)
1164+
eng.resource_manager.add_abort_req_ids.assert_called_once()
11581165

11591166
eng._control_resume(ControlRequest(request_id="ctrl2", method="resume"))
11601167
self.assertFalse(eng.is_paused)

0 commit comments

Comments
 (0)