LLM 批量推理的性能调度:排队策略与优先级分级的工程化实现

LLM 批量推理的性能调度:排队策略与优先级分级的工程化实现

一、1000 个请求同时到达的调度难题

一个 AI 代码审查服务,每天上午 10 点(开发者的集中提交时间)会收到 800-1200 个审查请求。初始的实现是 FIFO 处理——先到的先处理,后到的排队。问题是:

  • 一个紧急的 hotfix 审查排在了一个计划了两个月的重构的 120 个文件变更之后
  • 时长分布极不均匀:简单审查 15 秒,复杂审查 120 秒
  • FIFO 队列中的短任务被长任务阻塞,平均等待时间达到 3.8 分钟

这不是计算资源不够(8 个 GPU 并行处理),而是调度策略让资源被无效占用。需要引入优先级分级和多级反馈队列。

二、优先级分级的任务调度

2.1 多级反馈队列的实现

// 多级反馈队列调度器 type MFQScheduler struct { queues []*PriorityQueue // 多个优先级队列 workers int // 可用 Worker 数 timeSlice time.Duration // 每个任务的时间片 mu sync.Mutex } type PriorityQueue struct { tasks []*InferenceTask priority int // 0=最高, 3=最低 weight int // 资源分配权重 mu sync.Mutex } func (s *MFQScheduler) Submit(task *InferenceTask) error { if task.Priority < 0 || task.Priority >= len(s.queues) { return fmt.Errorf("非法优先级: %d", task.Priority) } q := s.queues[task.Priority] q.mu.Lock() q.tasks = append(q.tasks, task) q.mu.Unlock() return nil } func (s *MFQScheduler) Schedule(ctx context.Context) { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: s.dispatchRound() case <-ctx.Done(): return } } } func (s *MFQScheduler) dispatchRound() { totalWeight := 0 for _, q := range s.queues { totalWeight += q.weight } // 按权重分配 Worker 数 allocated := make([]int, len(s.queues)) remaining := s.workers for i, q := range s.queues { allocated[i] = s.workers * q.weight / totalWeight remaining -= allocated[i] } allocated[0] += remaining // 剩余全给最高优先级 // 各队列按分配数调度任务 for i, q := range s.queues { q.mu.Lock() for j := 0; j < allocated[i] && len(q.tasks) > 0; j++ { task := q.tasks[0] q.tasks = q.tasks[1:] q.mu.Unlock() go s.processTask(task) q.mu.Lock() } q.mu.Unlock() } }

2.2 优先级反转的预防

当高优先级任务需要等待低优先级任务持有的锁时,会发生优先级反转:

// 优先级继承机制:低优先级任务继承等待它的最高优先级 type PriorityTask struct { task *InferenceTask priority int inheritors []*InferenceTask // 等待此任务完成的高优先级任务 } func (s *MFQScheduler) handleDependency(task *InferenceTask) { for _, dep := range task.DependsOn { // 如果低优先级任务被高优先级任务等待 if dep.Priority > task.Priority { dep.Priority = task.Priority // 提升优先级 } } }

三、排队延迟的监控

// 排队延迟追踪器 type QueueMetrics struct { waitTimes []time.Duration mu sync.Mutex } func (m *QueueMetrics) RecordWaitTime(priority int, wait time.Duration) { m.mu.Lock() defer m.mu.Unlock() // 如果等待时间超过阈值,记录告警 thresholds := []time.Duration{ 10 * time.Second, // P0: 10s 60 * time.Second, // P1: 60s 5 * time.Minute, // P2: 5min } if priority < len(thresholds) && wait > thresholds[priority] { log.Printf("告警: P%d 任务等待时间 %.1fs 超过阈值 %.1fs", priority, wait.Seconds(), thresholds[priority].Seconds()) } }

四、边界与权衡

优先级的滥用:如果所有用户都把任务标记为 P0,优先级系统失效。需要在任务提交时做强制检查——P0 任务需要特定的触发条件(如合并到 hotfix 分支),不能手动标记。

饥饿问题:低优先级任务可能永远得不到执行。使用"老化"(Aging)机制——任务在低优先级队列中每等待 30 分钟,优先级自动升一级。

FIFO 的简单性优势:如果任务的执行时间分布均匀(标准差 < 平均值),FIFO 是最简单且公平的调度策略。在验证任务时长分布之前,不要过早引入优先级系统——它带来的复杂性可能超过收益。

五、总结

LLM 批量推理的调度核心是"短任务不被长任务阻塞"和"紧急任务不被普通任务阻塞"。多级反馈队列是最简洁的实现——4 级优先级 + 时间片轮转 + 老化机制,平衡了公平性和效率。

实施步骤:先观察任务执行时间的分布(P50/P95/P99),如果 P99/P50 > 10,说明分布极度不均匀——引入优先级系统的收益最大。然后从 P0/P1 两级开始(紧急 vs 正常),运行稳定后再扩展到 4 级。指标驱动的决策:平均等待时间下降 > 50%,才证明调度优化有效。