
FairBatching深度解析:解决LLM推理中的预填充饥饿问题
引言
在大规模语言模型(LLM)推理服务中,批处理调度策略直接影响着服务质量和资源利用效率。随着大模型推理需求的爆炸式增长,如何在保证高吞吐量的同时维持良好的用户体验,成为了AI基础设施领域的重要挑战。然而,现有的调度策略如Sarathi的stall-free batching虽然在吞吐量方面表现优异,却存在一个根本性问题:过度优先解码任务,导致预填充(prefill)任务在突发流量下遭遇严重的首令牌时间(TTFT)违规。这种”预填充饥饿”现象不仅影响用户体验,也造成了资源分配的严重失衡。
具体来说,在传统的stall-free batching策略中,系统会将长序列的预填充任务分割成多个小块,并与正在进行的解码任务混合执行。这种策略虽然能够最大化GPU利用率,避免计算单元空闲,但在高负载场景下会产生严重的不公平性问题:新到达的预填充请求可能需要等待大量正在进行的解码任务完成才能获得计算资源,导致TTFT显著增加,甚至违反服务等级目标(SLO)。
近期上海交通大学提出的FairBatching调度策略,通过引入信封线SLO跟踪机制(Envelope SLO Tracker)、自适应批容量确定机制和三阶段批打包算法,有效解决了这一公平性问题。实验结果表明,FairBatching能够在保持高GPU利用率的同时,将TTFT P99延迟降低最多2.29倍,并显著提升不同请求类型间的公平性。
本文将深入解析FairBatching的核心技术原理,详细分析其与现有方案的差异,并探讨其在主流推理框架vLLM中的实现可能性和实际部署考虑。通过本文的分析,读者将能够理解如何在复杂的LLM推理系统中平衡效率与公平性这两个看似矛盾的目标。
现有调度策略的根本缺陷
预填充饥饿现象的根源分析
要理解FairBatching的创新之处,首先需要深入分析现有调度策略的根本缺陷。传统的LLM推理调度器通常在两个极端之间摇摆:要么采用严格的FIFO策略优先处理预填充任务以保证TTFT,要么采用激进的吞吐量优化策略优先处理解码任务。
Sarathi的stall-free batching代表了后者,它通过将长序列的预填充任务分割成固定大小的块(chunks),并将这些块与正在进行的解码任务交错执行,从而避免GPU计算单元的空闲等待。这种策略在稳态负载下确实能够实现接近理论最大值的GPU利用率。
然而,问题出现在负载动态变化的场景中。当系统突然接收到大量新的预填充请求时,这些请求必须排队等待当前正在执行的解码任务完成。由于每个解码任务只需要处理一个token,而预填充任务需要处理整个prompt序列,这种不对称性导致了严重的资源竞争问题。
让我们通过一个具体的例子来说明这个问题:
假设系统当前正在处理100个解码任务,每个任务每步生成1个token。同时,有10个新的预填充请求到达,每个请求的prompt长度为1000 tokens。在stall-free batching策略下,这10个预填充请求会被分割成1000个chunk(每个chunk处理1个token),并与现有的100个解码任务混合执行。
如果GPU的最大批处理容量为128,那么每个step只能处理128个请求。这意味着每个预填充chunk平均需要等待约8个step才能被执行(100解码任务 + 1000预填充chunks ≈ 1100 total,1100/128 ≈ 8.6 steps)。对于长度为1000的prompt,总的等待时间将是巨大的,远远超过合理的TTFT SLO。
量化分析:预填充饥饿的影响
通过实验数据可以更清晰地看到预填充饥饿现象的严重程度。在标准的Llama-2-7B模型上,使用Sarathi的stall-free batching策略,在高负载场景下(并发请求数 > 200),TTFT P99延迟可以达到1.2秒以上,而P50延迟仅为200毫秒左右。这种巨大的尾部延迟差异正是预填充饥饿的直接体现。
此外,预填充饥饿还会导致以下问题:
- 服务质量不稳定:用户无法获得一致的响应体验,特别是在系统负载波动时
- 资源浪费:长时间等待的预填充请求可能导致客户端超时重试,进一步加剧系统负载
- 业务指标下降:在实际应用中,高TTFT会直接影响用户留存率和满意度
传统解决方案的局限性分析
面对预填充饥饿问题,业界提出了一些传统解决方案,但它们都存在明显的局限性:
静态批大小限制:通过限制最大批处理大小来为预填充任务保留资源。这种方法的问题在于缺乏动态适应性——在低负载时会造成资源浪费,在高负载时又无法有效保护预填充任务。
简单优先级队列:为预填充任务分配更高的优先级。这种方法虽然能够改善TTFT,但会显著降低整体吞吐量,因为频繁的上下文切换和小批次执行会降低GPU利用率。
时间片轮转:交替执行预填充和解码任务。这种方法在理论上看起来公平,但实际上很难找到合适的轮转周期,而且无法根据实际的SLO要求进行动态调整。
这些传统方法的根本问题在于它们都采用了静态的、启发式的调度策略,缺乏对系统状态和SLO要求的实时感知能力。更重要的是,它们没有建立一个统一的框架来同时考虑TTFT和TPOT(Time Per Output Token)这两个关键指标。
FairBatching核心技术原理
信封线SLO跟踪机制的数学基础
FairBatching的核心创新在于提出了信封线SLO跟踪机制(Envelope SLO Tracker),这是一个基于松弛时间(slack time)概念的统一SLO管理框架。松弛时间是指在不违反SLO的前提下,某个操作还可以延迟的时间长度。
Slack Time的统一建模
传统的调度系统通常分别处理TTFT和TPOT,使用不同的度量标准和阈值。FairBatching的关键洞察是,这两种SLO实际上都可以转换为等效的slack时间度量:
- 对于预填充任务:slack = TTFT_SLO - (当前已用时间 + 预计剩余时间)
- 对于解码任务:slack = TPOT_SLO - (预计下一个token的生成时间 - 上一个token的生成时间)
通过这种统一的slack时间表示,FairBatching能够在一个共同的维度上比较和管理所有类型的请求,无论它们处于预填充阶段还是解码阶段。
详细的实现机制
class EnvelopeSLOTracker:
"""
统一跟踪TTFT和TPOT的SLO达成情况
使用松弛时间作为统一的度量标准
"""
def __init__(self, ttft_slo_ms: float, tpot_slo_ms: float, gpu_profile: GPUProfile):
self.ttft_slo = ttft_slo_ms
self.tpot_slo = tpot_slo_ms
self.gpu_profile = gpu_profile
self.request_progress = {} # request_id -> progress_info
self.token_processing_times = deque(maxlen=1000) # 用于TPOT预测的历史数据
def calculate_slack(self, request_id: str, current_time: float) -> float:
"""
计算请求的slack时间
正值表示还有余量,负值表示已违规
"""
if request_id not in self.request_progress:
return float('inf') # 新请求,假设无限slack
req_info = self.request_progress[request_id]
if req_info.is_prefill:
# 预填充任务的slack计算
elapsed = current_time - req_info.arrival_time
remaining_tokens = req_info.total_tokens - req_info.processed_tokens
# 使用GPU性能模型估计剩余处理时间
estimated_prefill_time = self.estimate_prefill_time(
remaining_tokens,
req_info.context_length
)
# 对于预填充任务,还需要考虑后续解码时间
# 因为TTFT SLO包含整个预填充完成时间
estimated_completion = elapsed + estimated_prefill_time
slack = self.ttft_slo - estimated_completion
else:
# 解码任务的slack计算
if req_info.processed_tokens == 0:
# 还未开始处理,计算TTFT slack
elapsed = current_time - req_info.arrival_time
estimated_first_token_time = self.estimate_first_token_time(req_info)
slack = self.ttft_slo - (elapsed + estimated_first_token_time)
elif req_info.processed_tokens == 1:
# 第一个token刚完成,仍然关注TTFT
elapsed = current_time - req_info.arrival_time
slack = self.ttft_slo - elapsed
else:
# 多个token已完成,关注TPOT
# 使用历史数据预测下一个token的处理时间
predicted_tpot = self.predict_next_tpot(req_info)
expected_next_completion = current_time + predicted_tpot
actual_tpot = expected_next_completion - req_info.last_token_time
slack = self.tpot_slo - actual_tpot
return slack
def estimate_prefill_time(self, tokens: int, context_length: int) -> float:
"""基于GPU性能模型估计预填充时间"""
# 简化的性能模型:考虑tokens数量和上下文长度
base_time_per_token = self.gpu_profile.prefill_time_per_token
context_factor = 1.0 + (context_length / 1000.0) * 0.1 # 上下文长度影响
return tokens * base_time_per_token * context_factor
def predict_next_tpot(self, req_info) -> float:
"""基于历史数据预测下一个TPOT"""
if len(req_info.token_times) < 2:
return self.gpu_profile.avg_decode_time
# 使用最近几个token的平均时间作为预测
recent_tpot = np.mean(req_info.token_times[-5:])
return recent_tpot
def update_progress(self, request_id: str, tokens_generated: int, processing_time: float):
"""更新请求进度和性能数据"""
if request_id not in self.request_progress:
return
req_info = self.request_progress[request_id]
req_info.processed_tokens += tokens_generated
req_info.last_token_time = time.time()
# 记录处理时间用于后续预测
if tokens_generated == 1:
req_info.token_times.append(processing_time)
self.token_processing_times.append(processing_time)
# 更新上下文长度
req_info.context_length += tokens_generated
这个实现不仅提供了slack时间的计算,还包含了基于GPU性能模型的时间预测和基于历史数据的动态调整。通过这种方式,FairBatching能够准确预测每个请求的未来行为,并据此做出最优的调度决策。
自适应批容量确定机制的深度分析
基于信封线SLO跟踪的结果,FairBatching采用了一个精细的时间预算模型来动态调整批处理容量。这个机制的核心思想是:批处理容量不应该是一个固定的配置参数,而应该根据当前系统的SLO健康状况动态调整。
时间预算模型的理论基础
FairBatching将整个调度系统视为一个时间预算管理者。每个请求都有一个时间预算(即SLO),而调度器的任务是在不超出任何请求时间预算的前提下,最大化资源利用率。
时间预算的状态可以通过slack时间的分布来表征:
- 正slack:系统有时间余量,可以承担更大的批处理开销
- 负slack:系统时间紧张,需要采取保守的调度策略
- slack分布:不仅要看平均值,还要看分布的尾部,因为SLO通常是P99或P95级别的要求
实现细节与优化
def compute_adaptive_batch_capacity(
active_requests: List[Request],
pending_requests: List[Request],
slo_tracker: EnvelopeSLOTracker,
gpu_profile: GPUProfile,
safety_margin: float = 0.1
) -> Tuple[int, Dict[str, Any]]:
"""
基于slack分析动态计算批容量
返回容量和详细的分析报告
"""
current_time = time.time()
# 分别计算活跃请求和等待请求的slack
active_slacks = []
pending_slacks = []
for req in active_requests:
slack = slo_tracker.calculate_slack(req.id, current_time)
active_slacks.append(slack)
for req in pending_requests:
slack = slo_tracker.calculate_slack(req.id, current_time)
pending_slacks.append(slack)
# 计算关键统计指标
if active_slacks:
active_avg_slack = np.mean(active_slacks)
active_p95_slack = np.percentile(active_slacks, 95)
active_p5_slack = np.percentile(active_slacks, 5)
else:
active_avg_slack = float('inf')
active_p95_slack = float('inf')
active_p5_slack = float('inf')
if pending_slacks:
pending_avg_slack = np.mean(pending_slacks)
pending_min_slack = min(pending_slacks)
else:
pending_avg_slack = float('inf')
pending_min_slack = float('inf')
# 基础容量设置
base_capacity = gpu_profile.max_batch_size
# 决策逻辑:综合考虑多个因素
decision_factors = {
'active_avg_slack': active_avg_slack,
'active_p5_slack': active_p5_slack, # 关注最紧张的请求
'pending_min_slack': pending_min_slack, # 关注最紧急的等待请求
'pending_count': len(pending_requests),
'active_count': len(active_requests)
}
# 主要决策逻辑
if pending_min_slack < -50: # 有请求即将严重违规
# 紧急模式:最小化批容量以快速响应
capacity = max(1, int(base_capacity * 0.3))
mode = "emergency"
elif active_p5_slack < -20: # 有活跃请求时间紧张
# 保守模式:减少容量以保护SLO
capacity = int(base_capacity * 0.6)
mode = "conservative"
elif pending_avg_slack > 100 and len(pending_requests) > 10: # 有大量宽松的等待请求
# 扩展模式:增加容量以提高吞吐量
capacity_increase = min(int(active_avg_slack / 20), 8)
capacity = min(base_capacity + capacity_increase, gpu_profile.hard_limit)
mode = "expansion"
elif active_avg_slack > 50: # 系统整体宽松
# 宽松模式:适度增加容量
capacity = min(base_capacity + 3, gpu_profile.hard_limit)
mode = "relaxed"
else:
# 正常模式:使用基准容量
capacity = base_capacity
mode = "normal"
# 应用安全边际
capacity = int(capacity * (1 - safety_margin))
# 确保最小容量
capacity = max(capacity, 1)
analysis_report = {
'mode': mode,
'factors': decision_factors,
'final_capacity': capacity
}
return capacity, analysis_report
这个实现比简单的平均slack判断更加精细,它考虑了slack分布的多个百分位点、等待队列的状态以及不同类型请求的紧急程度。通过这种方式,FairBatching能够在各种负载条件下都做出合理的容量决策。
三阶段批打包策略的完整实现
FairBatching采用三阶段的批打包策略,这是其实现公平与效率平衡的关键机制。每个阶段都有明确的目标和约束条件,确保在不同系统状态下都能做出最优的调度决策。
阶段一:紧急解码任务保护
第一阶段专注于保护那些即将违反SLO的解码任务。这是因为解码任务通常已经投入了大量的计算资源,如果中途被长时间中断,不仅会影响用户体验,还会造成资源浪费。
def get_urgent_decode_requests(self, decode_requests: List[Request],
current_time: float, violation_threshold: float) -> List[Request]:
"""获取濒临SLO违规的解码请求"""
urgent_requests = []
for req in decode_requests:
slack = self.slo_tracker.calculate_slack(req.id, current_time)
if slack <= violation_threshold:
# 计算违规的严重程度
urgency_score = -slack # 负slack越大,越紧急
urgent_requests.append((req, urgency_score, slack))
# 按紧急程度排序,最紧急的优先
urgent_requests.sort(key=lambda x: x[1], reverse=True)
return [(req, slack) for req, _, slack in urgent_requests]
阶段二:预填充任务公平调度
第二阶段专门处理预填充任务,这是FairBatching区别于传统策略的关键。通过为预填充任务预留专门的调度机会,FairBatching有效避免了预填充饥饿问题。
在选择预填充任务时,FairBatching采用了一种智能的chunking策略:
def select_prefill_chunk(self, pending_prefill: List[Request],
available_capacity: int, current_time: float) -> List[Request]:
"""选择合适的预填充任务块"""
if not pending_prefill or available_capacity <= 0:
return []
# 计算每个预填充请求的紧急程度
prefill_candidates = []
for req in pending_prefill:
slack = self.slo_tracker.calculate_slack(req.id, current_time)
urgency = -slack if slack < 0 else 0 # 只有违规的才紧急
cost = self.estimate_prefill_cost(req)
# 如果容量不足以处理整个请求,考虑chunking
if cost > available_capacity:
# 创建chunk
chunk_size = min(available_capacity, req.remaining_tokens)
chunk_cost = self.estimate_chunk_cost(chunk_size, req.context_length)
if chunk_cost <= available_capacity:
chunk = self.create_prefill_chunk(req, chunk_size)
prefill_candidates.append((chunk, urgency, chunk_cost))
else:
prefill_candidates.append((req, urgency, cost))
# 按紧急程度排序
prefill_candidates.sort(key=lambda x: x[1], reverse=True)
# 贪心选择
selected = []
current_load = 0
for candidate, urgency, cost in prefill_candidates:
if current_load + cost <= available_capacity:
selected.append(candidate)
current_load += cost
return selected
阶段三:非紧急任务填充
第三阶段利用剩余的容量来处理非紧急的解码任务,以最大化GPU利用率。这部分实现相对简单,主要是按照一定的优先级顺序填充剩余容量。
def select_non_urgent_decode(self, active_decode: List[Request],
remaining_capacity: int, current_time: float) -> List[Request]:
"""选择非紧急的解码任务填充剩余容量"""
non_urgent = []
for req in active_decode:
slack = self.slo_tracker.calculate_slack(req.id, current_time)
if slack > -10: # 不是紧急任务
cost = self.estimate_decode_cost(req)
non_urgent.append((req, slack, cost))
# 按slack排序,slack大的(更宽松的)优先
non_urgent.sort(key=lambda x: x[1], reverse=True)
selected = []
current_load = 0
for req, slack, cost in non_urgent:
if current_load + cost <= remaining_capacity:
selected.append(req)
current_load += cost
return selected
完整的调度流程
将三个阶段组合起来,形成完整的FairBatching调度流程:
def form_batch(self, pending_prefill: List[Request],
active_decode: List[Request],
current_capacity: int) -> Batch:
batch = Batch()
current_time = time.time()
# Phase 1: 优先处理濒临SLO违规的解码请求
urgent_decode = self.get_urgent_decode_requests(
active_decode, current_time, violation_threshold=-10
)
# 添加紧急解码任务
urgent_capacity_used = 0
for req, slack in urgent_decode:
cost = self.estimate_decode_cost(req)
if urgent_capacity_used + cost <= current_capacity:
batch.add_request(req)
urgent_capacity_used += cost
else:
break
# Phase 2: 立即调度预填充请求
available_capacity = current_capacity - urgent_capacity_used
if available_capacity > 0:
prefill_chunk = self.select_prefill_chunk(
pending_prefill, available_capacity, current_time
)
for chunk in prefill_chunk:
batch.add_request(chunk)
# Phase 3: 填充剩余容量
remaining = current_capacity - batch.get_total_cost()
if remaining > 0:
non_urgent = self.select_non_urgent_decode(
active_decode, remaining, current_time
)
for req in non_urgent:
batch.add_request(req)
return batch
这种三阶段策略确保了在任何系统状态下,FairBatching都能在保证SLO的前提下最大化资源利用率。
与现有方案的全面对比分析
性能指标的详细对比
根据论文中的实验数据,FairBatching相比Sarathi的stall-free batching在多个关键指标上均有显著改善。这些实验在Llama-2-7B和Llama-2-13B模型上进行,使用了真实的用户请求trace。
| 指标 | Sarathi | FairBatching | 改善幅度 | 统计显著性 |
|---|---|---|---|---|
| TTFT P99延迟 | 1.2s | 0.52s | 2.29倍降低 | p < 0.001 |
| TTFT P95延迟 | 0.8s | 0.38s | 2.11倍降低 | p < 0.001 |
| TTFT P50延迟 | 0.2s | 0.18s | 1.11倍降低 | p < 0.05 |
| TPOT P99延迟 | 45ms | 42ms | 1.07倍降低 | p < 0.1 |
| 解码任务公平性(CV) | 0.65 | 0.23 | 2.83倍改善 | p < 0.001 |
| GPU利用率 | 85% | 82% | 轻微下降 | - |
| 单节点容量(QPS) | 100 | 120 | 20%提升 | p < 0.001 |
| SLO违规率 | 15% | 2% | 7.5倍降低 | p < 0.001 |
其中,CV(Coefficient of Variation,变异系数)用于衡量解码任务间的公平性,值越小表示公平性越好。SLO违规率是指TTFT或TPOT超过预设阈值的请求比例。
不同负载场景下的表现
FairBatching的优势在不同的负载场景下表现各异:
低负载场景(并发请求数 < 50):
- 两种策略性能相近
- FairBatching略有优势,但差异不显著
- GPU利用率基本相同
中等负载场景(50 ≤ 并发请求数 ≤ 200):
- FairBatching开始显现优势
- TTFT P99延迟降低约1.5倍
- SLO违规率显著降低
高负载场景(并发请求数 > 200):
- FairBatching优势最为明显
- TTFT P99延迟降低2倍以上
- 系统稳定性显著提升
- 单节点容量提升20%
突发流量场景:
- Sarathi表现极差,TTFT P99可达到2秒以上
- FairBatching能够快速适应,TTFT P99控制在0.6秒以内
- 恢复时间缩短50%
架构设计的哲学差异
传统的调度策略往往采用单一的优化目标(通常是吞吐量最大化),而FairBatching的设计体现了多目标优化的思想:
# 传统调度器的优化目标
# maximize throughput = f(batch_size, gpu_utilization)
class TraditionalScheduler:
def __init__(self, max_batch_size):
self.max_batch_size = max_batch_size
def schedule(self, requests):
# 简单的贪心策略:尽可能填满批次
batch = []
current_size = 0
for req in sorted(requests, key=lambda x: x.arrival_time):
if current_size + req.cost <= self.max_batch_size:
batch.append(req)
current_size += req.cost
return batch
# FairBatching的多目标优化
# minimize max_violation + λ * (1 - gpu_utilization)
# subject to: all_slo_constraints
class FairBatchingScheduler:
def __init__(self, slo_tracker, gpu_profile):
self.slo_tracker = slo_tracker
self.gpu_profile = gpu_profile
self.lambda_param = 0.1 # 权衡参数
def schedule(self, requests):
# 复杂的多阶段决策过程
pending_prefill = [r for r in requests if r.is_prefill and not r.started]
active_decode = [r for r in requests if not r.is_prefill and r.started]
# 动态分析当前状态
current_capacity, analysis = compute_adaptive_batch_capacity(
active_decode, pending_prefill, self.slo_tracker, self.gpu_profile
)
# 基于SLO跟踪的三阶段决策
batch = self.form_batch(pending_prefill, active_decode, current_capacity)
return batch
这种设计哲学的差异反映了对LLM推理系统本质理解的不同:传统方法将推理系统视为纯粹的计算引擎,而FairBatching将其视为需要同时满足多个服务质量目标的复杂服务系统。
在vLLM中的实现思路与挑战
核心组件集成方案
要在vLLM中集成FairBatching,需要对现有架构进行适度的扩展和修改。vLLM的调度器架构相对模块化,这为FairBatching的集成提供了良好的基础。
主要修改点
- Scheduler类扩展:添加FairBatching调度逻辑
- SLO跟踪模块:实现信封线SLO跟踪器
- 批容量计算模块:实现自适应批容量确定
- 请求状态跟踪:增强请求对象以支持slack计算
详细的集成代码
# vLLM scheduler.py 中的集成示例
from vllm.core.scheduler import Scheduler, SchedulingPolicy
from vllm.core.interfaces import RequestMetadata
import time
class FairBatchingRequest(RequestMetadata):
"""扩展的请求元数据,支持FairBatching所需的状态跟踪"""
def __init__(self, original_request, ttft_slo_ms, tpot_slo_ms):
super().__init__(original_request)
self.arrival_time = time.time()
self.ttft_slo_ms = ttft_slo_ms
self.tpot_slo_ms = tpot_slo_ms
self.processed_tokens = 0
self.last_token_time = self.arrival_time
self.token_times = []
self.context_length = len(original_request.prompt_token_ids)
self.is_prefill = True
self.slack_cache = None
self.slack_cache_time = 0
def mark_as_decode(self):
"""标记请求进入解码阶段"""
self.is_prefill = False
def update_token_stats(self, token_time):
"""更新token处理统计信息"""
self.processed_tokens += 1
self.token_times.append(token_time)
self.last_token_time = time.time()
class FairBatchingScheduler(Scheduler):
def __init__(self, scheduler_config, cache_config, lora_config=None):
super().__init__(scheduler_config, cache_config, lora_config)
# FairBatching特定配置
self.ttft_slo_ms = getattr(scheduler_config, 'ttft_slo_ms', 500.0)
self.tpot_slo_ms = getattr(scheduler_config, 'tpot_slo_ms', 100.0)
self.violation_threshold_ms = getattr(scheduler_config, 'violation_threshold_ms', -10.0)
self.adaptive_scaling_enabled = getattr(scheduler_config, 'adaptive_scaling_enabled', True)
# 初始化SLO跟踪器
self.slo_tracker = EnvelopeSLOTracker(
ttft_slo_ms=self.ttft_slo_ms,
tpot_slo_ms=self.tpot_slo_ms,
gpu_profile=self._build_gpu_profile()
)
# FairBatching策略
self.policy = FairBatchingPolicy(
slo_tracker=self.slo_tracker,
violation_threshold=self.violation_threshold_ms
)
# 请求映射表
self.request_map = {}
def add_seq_group(self, seq_group):
"""添加新的序列组,创建FairBatching请求对象"""
fb_request = FairBatchingRequest(
seq_group,
self.ttft_slo_ms,
self.tpot_slo_ms
)
self.request_map[seq_group.request_id] = fb_request
super().add_seq_group(seq_group)
def schedule(self):
"""FairBatching调度主逻辑"""
# 获取等待队列和运行队列
waiting_queue = []
running_queue = []
# 转换vLLM内部表示为FairBatching请求
for seq_group in self.waiting:
if seq_group.request_id in self.request_map:
waiting_queue.append(self.request_map[seq_group.request_id])
for seq_group in self.running:
if seq_group.request_id in self.request_map:
req = self.request_map[seq_group.request_id]
# 检查是否已完成预填充
if seq_group.is_prefill_complete():
req.mark_as_decode()
running_queue.append(req)
# 计算自适应批容量
if self.adaptive_scaling_enabled:
current_capacity, _ = compute_adaptive_batch_capacity(
running_queue, waiting_queue, self.slo_tracker,
self.slo_tracker.gpu_profile
)
else:
current_capacity = self.scheduler_config.max_num_seqs
# 应用FairBatching策略
scheduled_requests = self.policy.form_batch(
waiting_queue, running_queue, current_capacity
)
# 转换回vLLM格式
scheduled_seq_groups = []
for req in scheduled_requests:
# 找到对应的seq_group
for seq_group in self.waiting + self.running:
if seq_group.request_id == req.request_id:
scheduled_seq_groups.append(seq_group)
break
# 调用父类的调度逻辑
return super().schedule_from_selected(scheduled_seq_groups)
def _build_gpu_profile(self):
"""基于当前GPU配置构建性能模型"""
# 这里需要根据实际的GPU型号和配置进行调整
# 可以通过基准测试自动校准
return GPUProfile(
max_batch_size=self.scheduler_config.max_num_seqs,
hard_limit=self.scheduler_config.max_num_seqs * 1.5,
prefill_time_per_token=0.5, # ms per token
avg_decode_time=20.0, # ms per token
memory_bandwidth_gb=1555 # GB/s for A100
)
class FairBatchingPolicy(SchedulingPolicy):
def __init__(self, slo_tracker, violation_threshold=-10.0):
self.slo_tracker = slo_tracker
self.violation_threshold = violation_threshold
def form_batch(self, waiting_queue, running_queue, capacity):
# 实现三阶段批打包逻辑
# ... (前面详细实现的代码)
pass
配置参数扩展与向后兼容
为了保持向后兼容性,FairBatching的配置应该作为可选扩展:
# vLLM配置扩展
@dataclass
class SchedulerConfig:
# 现有配置...
max_num_seqs: int = 256
max_model_len: int = 2048
# FairBatching扩展配置(可选)
enable_fair_batching: bool = False
ttft_slo_ms: Optional[float] = None
tpot_slo_ms: Optional[float] = None
violation_threshold_ms: float = -10.0
adaptive_scaling_enabled: bool = True
def __post_init__(self):
if self.enable_fair_batching:
if self.ttft_slo_ms is None:
self.ttft_slo_ms = 500.0 # 默认500ms
if self.tpot_slo_ms is None:
self.tpot_slo_ms = 100.0 # 默认100ms
集成挑战与解决方案
在实际集成过程中可能会遇到以下挑战:
挑战1:vLLM内部状态管理复杂性 vLLM使用复杂的序列组(SequenceGroup)和块表(BlockTable)管理机制,FairBatching需要与这些机制无缝集成。
解决方案:通过包装器模式,在不修改核心逻辑的前提下扩展功能。
挑战2:性能开销控制 FairBatching的额外计算开销可能影响调度性能。
解决方案:
- 使用缓存机制避免重复计算
- 采用增量更新策略
- 在调度间隔较长时进行复杂的计算
挑战3:GPU内存管理 FairBatching的动态批容量可能与vLLM的静态内存分配策略冲突。
解决方案:
- 修改内存池大小以适应动态批容量
- 实现更灵活的内存分配策略
- 在容量调整时进行内存重新分配
实际部署考虑与最佳实践
性能开销的详细评估
FairBatching相比传统调度策略会引入一定的计算开销,主要体现在以下几个方面:
- SLO跟踪开销:每个请求的状态跟踪和slack计算
- 时间复杂度:O(N),N为活跃请求数
- 实际开销:每个调度周期约1-2ms(在1000并发请求下)
- 批容量动态调整:实时的容量计算和调整
- 时间复杂度:O(N log N),主要来自排序操作
- 实际开销:每个调度周期约2-3ms
- 多阶段决策:复杂的调度决策过程
- 时间复杂度:O(N)
- 实际开销:每个调度周期约1-2ms
根据论文数据和我们的初步测试,这些开销通常在可接受范围内。在A100 GPU上,调度周期通常为10-50ms,FairBatching的额外开销约占调度周期的10-20%,对整体性能影响有限。GPU利用率仅轻微下降约2-3%,但换来的是显著改善的服务质量。
参数调优的系统化方法
在实际部署中,参数调优是确保FairBatching发挥最佳效果的关键。建议采用以下系统化方法:
1. 基准测试阶段
首先在代表性工作负载下进行基准测试,确定基础参数:
# 示例基准测试脚本
python benchmark_fairbatching.py \
--model llama-2-7b \
--workload real_trace.json \
--ttft-slo-range 200,1000 \
--tpot-slo-range 50,200 \
--output tuning_results.csv
2. 敏感性分析
对关键参数进行敏感性分析:
- TTFT SLO设置:通常设置为P95延迟目标的1.2-1.5倍
- TPOT SLO设置:通常设置为平均TPOT的2-3倍
- Violation阈值:建议从-10ms开始,根据实际效果调整
- 自适应调整敏感度:通过安全边际参数控制
3. 在线调优
在生产环境中实施在线调优:
class OnlineTuner:
def __init__(self, scheduler):
self.scheduler = scheduler
self.slo_violation_history = deque(maxlen=1000)
def adjust_parameters(self, current_metrics):
"""基于当前指标动态调整参数"""
violation_rate = current_metrics['slo_violation_rate']
if violation_rate > 0.05: # 违规率过高
# 收紧SLO或降低批容量
self.scheduler.ttft_slo_ms *= 0.9
self.scheduler.adaptive_scaling_enabled = True
elif violation_rate < 0.01: # 违规率过低
# 放宽SLO以提高吞吐量
self.scheduler.ttft_slo_ms *= 1.1
监控与可观测性
部署FairBatching后,需要建立完善的监控体系:
- SLO达成率监控:实时跟踪TTFT和TPOT的SLO达成情况
- 调度决策日志:记录每个调度周期的决策过程和参数
- 资源利用率监控:跟踪GPU利用率、内存使用等指标
- 公平性指标:监控不同请求类型间的公平性
# 监控指标示例
metrics = {
'ttft_p99_ms': 520,
'ttft_slo_achievement_rate': 0.98,
'tpot_p99_ms': 42,
'tpot_slo_achievement_rate': 0.99,
'gpu_utilization': 0.82,
'fairness_cv': 0.23,
'current_batch_capacity': 180,
'scheduling_mode': 'conservative'
}
未来发展方向与研究前沿
机器学习增强调度
未来的调度器可能会深度集成机器学习模型,通过历史数据学习和预测请求行为模式:
class MLEnhancedFairBatching(FairBatchingScheduler):
def __init__(self, model_path: str, feature_config: Dict):
super().__init__()
self.feature_extractor = FeatureExtractor(feature_config)
self.behavior_predictor = load_ml_model(model_path)
self.prediction_cache = LRUCache(maxsize=10000)
def predict_request_behavior(self, request: Request) -> Dict[str, float]:
"""预测请求的行为特征"""
cache_key = request.request_id
if cache_key in self.prediction_cache:
return self.prediction_cache[cache_key]
features = self.feature_extractor.extract(request)
predictions = self.behavior_predictor.predict(features)
# 缓存预测结果
self.prediction_cache[cache_key] = predictions
return predictions
def calculate_slack(self, request_id: str, current_time: float) -> float:
"""使用ML预测增强的slack计算"""
predictions = self.predict_request_behavior(
self.request_map[request_id]
)
# 使用预测结果改进时间估计
if predictions['is_long_running']:
# 长运行请求可能需要特殊处理
return self._calculate_slack_with_prediction(
request_id, current_time, predictions
)
else:
return super().calculate_slack(request_id, current_time)
多租户公平性与资源隔离
在多租户环境中,FairBatching可以进一步扩展以支持跨租户的公平性保障:
- 租户级别SLO:为每个租户设置独立的SLO目标
- 资源配额管理:确保每个租户获得承诺的资源份额
- 跨租户公平性:在租户间实现公平的资源分配
class MultiTenantFairBatching(FairBatchingScheduler):
def __init__(self, tenant_configs: Dict[str, TenantConfig]):
super().__init__()
self.tenant_configs = tenant_configs
self.tenant_slo_trackers = {
tenant_id: EnvelopeSLOTracker(
config.ttft_slo_ms, config.tpot_slo_ms
)
for tenant_id, config in tenant_configs.items()
}
def form_batch(self, waiting_queue, running_queue, capacity):
# 按租户分组请求
tenant_requests = defaultdict(lambda: {'waiting': [], 'running': []})
for req in waiting_queue:
tenant_requests[req.tenant_id]['waiting'].append(req)
for req in running_queue:
tenant_requests[req.tenant_id]['running'].append(req)
# 为每个租户分配容量
tenant_capacities = self._allocate_capacity_by_tenant(
tenant_requests, capacity
)
# 分别调度每个租户的请求
final_batch = []
for tenant_id, capacities in tenant_capacities.items():
tenant_batch = self._schedule_tenant_requests(
tenant_requests[tenant_id]['waiting'],
tenant_requests[tenant_id]['running'],
capacities
)
final_batch.extend(tenant_batch)
return final_batch
硬件感知调度
未来的FairBatching可能会更加深度地集成硬件特性:
- 内存带宽感知:根据GPU内存带宽动态调整批处理策略
- 计算单元利用率:监控SM利用率并据此调整调度
- 多GPU协同:在多GPU系统中实现全局最优调度
总结与实践建议
FairBatching作为2025年提出的新一代LLM推理调度策略,通过信封线SLO跟踪机制、自适应批容量确定和三阶段批打包算法,有效解决了现有调度策略中的公平性问题。实验表明,该策略能够将TTFT尾延迟降低最多2.29倍,同时保持较高的GPU利用率(仅下降约3%)。
对于AI Infra工程师而言,FairBatching不仅提供了一个实用的调度解决方案,更重要的是展示了如何在复杂系统中平衡多个相互冲突的优化目标。随着大模型服务的普及和用户对服务质量要求的提高,这类公平性感知的调度策略将成为构建高质量推理服务的重要技术基础。
实践建议
- 渐进式部署:建议先在非关键业务中试用FairBatching,逐步验证其效果
- 参数调优:投入足够的时间进行参数调优,这是发挥FairBatching优势的关键
- 监控体系建设:建立完善的监控体系,实时跟踪FairBatching的效果
- 团队培训:确保运维团队理解FairBatching的工作原理和调优方法
尽管FairBatching在vLLM中的完整实现还需要进一步的工作,但其核心思想和技术框架已经为业界提供了重要的参考价值。我们预计在未来1-2年内,类似的公平性感知调度策略将成为主流LLM推理框架的标准功能。对于希望构建高质量大模型服务的团队来说,现在就开始研究和实验FairBatching将带来显著的竞争优势。