执行摘要
- 一句话:为 prefill bootstrap 注册添加指数退避重试
- 推荐动作:该 PR 修复了实际竞态问题,重试实现稳健(指数退避 + 抖动,异常链遍历),测试用例设计完整。建议学习其测试 mock 策略和日志分级设计。对于最终失败是否崩溃的讨论,可后续考虑添加配置项或强制退出选项。
功能与动机
在 PD 分离模式下,prefill 调度器在初始化时调用 register_to_bootstrap() 发布自身信息,但 bootstrap 服务器在 HTTP 服务器启动时才创建,两者没有同步。对于小模型(如 Qwen3-0.6B),prefill 初始化在几秒内完成,赶在 bootstrap 服务器启动前发起注册,导致连接被拒绝且无重试,最终所有请求 500,唯一恢复方式是重启 prefill 实例。详见 PR 描述中的日志序列。
实现拆解
- 修改
register_to_bootstrap 方法(conn.py),引入重试循环:设置 max_retries=5、initial_delay=1.0、max_delay=30.0。
- 每次循环先尝试 HTTP PUT 请求,若状态码为 200 则直接返回;若失败或抛出异常,记录 warning 日志并进入退避逻辑。
- 退避策略:
delay = min(initial_delay * 2^attempt, max_delay) * (0.75 + 0.25 * (time.monotonic() % 1)),确保抖动因子在 [0.75, 1.0) 内,最大延迟不超过 30 秒。
- 异常处理时遍历
__cause__ 链找到最底层异常,避免 urllib3 包装消息干扰日志可读性。
- 最后尝试后不 sleep,直接跳出循环并记录 error 日志。
- 修正 docstring 中 HTTP 方法描述(PUT 而非 POST)。
- 新增测试文件
test_register_to_bootstrap.py,注册到 CI stage-a-test-cpu 套件(预估 5 秒)。
- 通过 mock
requests.put 和 time.monotonic 覆盖 7 个用例:首次成功、重试成功、全部失败、嵌套异常、无嵌套异常、指数延迟验证、抖动不越界。
关键文件:
python/sglang/srt/disaggregation/common/conn.py(模块 连接层;类别 source;类型 core-logic;符号 register_to_bootstrap): 核心修改文件,实现重试逻辑
test/registered/unit/disaggregation/test_register_to_bootstrap.py(模块 连接层;类别 test;类型 test-coverage;符号 TestRegisterToBootstrap, test_succeeds_on_first_attempt, test_succeeds_after_retries, test_all_retries_exhausted): 新增单元测试文件,覆盖所有重试和异常场景
关键符号:register_to_bootstrap
关键源码片段
python/sglang/srt/disaggregation/common/conn.py
核心修改文件,实现重试逻辑
def register_to_bootstrap(self):
"""Register prefill server info to bootstrap server via HTTP PUT."""
if self.dist_init_addr:
host = NetworkAddress.parse(self.dist_init_addr).resolved().host
else:
host = self.bootstrap_host
bootstrap_na = NetworkAddress(host, self.bootstrap_port)
url = f"{bootstrap_na.to_url()}/route"
payload = {
"attn_tp_size": self.attn_tp_size,
# ... 其他 payload 字段省略 ...
"load_balance_method": self.server_args.load_balance_method,
}
max_retries, initial_delay, max_delay = 5, 1.0, 30.0 # 最多重试 5 次,退避基准 1s,上限 30s
for attempt in range(max_retries):
try:
response = requests.put(url, json=payload, timeout=5)
if response.status_code == 200:
logger.debug("Prefill successfully registered to bootstrap server.")
return # 成功则立即返回
logger.warning(
f"Prefill register attempt {attempt + 1}/{max_retries} failed: status {response.status_code}"
)
except Exception as e:
# 遍历 __cause__ 链以暴露根本原因(如 Connection refused),避免 urllib3 包装误导
cause = e
while cause.__cause__ is not None:
cause = cause.__cause__
logger.warning(
f"Prefill register attempt {attempt + 1}/{max_retries} failed: {cause}"
)
if attempt == max_retries - 1:
break # 最后一次尝试后不 sleep
delay = min(initial_delay * (2**attempt), max_delay) * (
0.75 + 0.25 * (time.monotonic() % 1) # 抖动系数 0.75~1.0,保证最大延迟不超过 max_delay
)
time.sleep(delay)
logger.error(
f"Prefill instance failed to register to bootstrap server after {max_retries} retries"
)
评论区精华
审阅者 ShangmingCai 在 conn.py 第 393 行评论 "Should we crash the server here?",质疑在重试全部耗尽后是否应该终止进程。该评论未收到回复,PR 随后获得批准合并,表明当前行为(仅记录 error 并继续运行)被接受,但设计选择未明确讨论。
- Should the server crash after all retries exhausted? (design): 未收到回复,PR 被批准合并,当前行为是记录 error 并继续运行。
风险与影响
- 风险:
- 启动延迟增加:最多可能增加约 31 秒(1+2+4+8+16 秒 + 抖动)的启动时间,但对于小模型可接受。
- 最终失败无防护:若 bootstrap 服务器永久不可达,prefill 会启动但未注册,后续请求必然失败,但日志已告知管理员。
- 无稳态影响:重试仅发生在启动阶段,不影响推理路径。
- 日志变更:失败日志从 error 降级为 warning,可能被监控忽略,但最终 error 仍保留。
- 影响:影响范围:仅 PD 分离模式下 prefill 实例的启动阶段;大模型(如 Qwen3-8B)因初始化时间长,通常不会触发重试,行为不变。影响程度:修复了小模型在 PD 分离下完全不可用的问题,提升了系统可靠性。对团队:测试覆盖完善,回归风险低。对用户:小模型 PD 分离部署不再 500。
- 风险标记:启动延迟增加, 最终失败未终止进程
关联脉络
参与讨论