第2297篇:GPU资源调度优化——如何用最少的GPU资源服务最多的请求
第2297篇:GPU资源调度优化——如何用最少的GPU资源服务最多的请求
适读人群:需要自托管AI模型、关心GPU成本的工程师和架构师 | 阅读时长:约15分钟 | 核心价值:掌握GPU资源调度的核心优化策略,在成本约束下最大化AI服务能力
公司自托管了几个模型(主要是Qwen和Embedding模型),用的是A100 GPU。运维同学跟我说,GPU平均利用率只有20-30%,但高峰期又经常有请求排队。
这是GPU调度的典型问题:平均利用率低,峰值利用率高。如果按峰值配置,大部分时间资源浪费;如果按平均配置,峰值时请求积压。
优化GPU利用率,需要从多个层面入手。
GPU低利用率的根本原因
先搞清楚为什么GPU利用率低。AI推理的GPU计算有几个特点:
计算阵发性:一个推理请求,模型的前向传播是GPU密集的,但准备数据、序列化结果是CPU操作,GPU在等待期间是空闲的。
批量大小(Batch Size)影响大:GPU是SIMD架构,同时处理多个请求比逐个处理效率高很多。batch size=1时,GPU算力可能只用到了10%;batch size=32时,利用率可以到80%。
请求到达不均匀:如果请求逐个串行处理,GPU大量时间在等下一个请求。
连续批处理(Continuous Batching)
这是目前LLM推理最重要的优化技术,也是vLLM的核心创新之一。
传统的静态批处理(Static Batching):等N个请求凑齐了再一起处理,结果较短的请求要等较长的请求,整体延迟高。
连续批处理(Continuous Batching):在每个decode步骤,动态决定哪些请求参与这一步的计算,已完成的请求立刻退出,新请求立刻加入,最大化GPU利用率:
// 模拟连续批处理调度器的核心逻辑
@Service
public class ContinuousBatchScheduler {
private final BlockingQueue<InferenceRequest> waitingQueue = new LinkedBlockingQueue<>();
private final Set<RunningRequest> runningRequests = new CopyOnWriteArraySet<>();
// GPU内存限制(以KV Cache token数量衡量)
private static final int MAX_RUNNING_TOKENS = 8192;
@Scheduled(fixedDelay = 1) // 每1ms检查一次,高频调度
public void scheduleStep() {
// 计算当前运行中的请求占用了多少token
int usedTokens = runningRequests.stream()
.mapToInt(RunningRequest::getUsedKvCacheTokens)
.sum();
// 从等待队列里尽可能多地加入新请求
while (usedTokens < MAX_RUNNING_TOKENS && !waitingQueue.isEmpty()) {
InferenceRequest req = waitingQueue.peek();
int requiredTokens = req.getPromptLength() + req.getMaxNewTokens();
if (usedTokens + requiredTokens <= MAX_RUNNING_TOKENS) {
waitingQueue.poll();
runningRequests.add(new RunningRequest(req));
usedTokens += requiredTokens;
} else {
break; // 剩余内存不够,等下一轮
}
}
if (runningRequests.isEmpty()) return;
// 执行一步decode(所有运行中的请求共享这一步)
Map<String, int[]> nextTokens = executeDecodeStep(runningRequests);
// 处理每个请求的decode结果
Iterator<RunningRequest> iter = runningRequests.iterator();
while (iter.hasNext()) {
RunningRequest req = iter.next();
int nextToken = nextTokens.get(req.getRequestId())[0];
req.addToken(nextToken);
// 检查是否完成(EOS token或达到max_new_tokens)
if (req.isFinished()) {
req.complete();
iter.remove(); // 从运行队列移除,下一步可以加入新请求
}
}
}
}在实际工程中,大家通常直接用vLLM(Python)或TensorRT-LLM来做LLM推理服务,不需要自己实现连续批处理。但Java服务可以通过HTTP/gRPC调用这些推理服务:
@Service
public class VllmClient {
private final WebClient webClient;
private final String vllmBaseUrl;
/**
* 流式调用vLLM(兼容OpenAI API格式)
*/
public Flux<String> streamGenerate(String prompt, GenerationConfig config) {
Map<String, Object> request = new HashMap<>();
request.put("prompt", prompt);
request.put("max_tokens", config.getMaxTokens());
request.put("temperature", config.getTemperature());
request.put("stream", true);
return webClient.post()
.uri(vllmBaseUrl + "/v1/completions")
.bodyValue(request)
.retrieve()
.bodyToFlux(String.class)
.filter(line -> line.startsWith("data: ") && !line.contains("[DONE]"))
.map(line -> extractTokenFromSSE(line));
}
/**
* 批量调用(非流式,高吞吐)
*/
public List<String> batchGenerate(List<String> prompts, GenerationConfig config) {
Map<String, Object> request = new HashMap<>();
request.put("prompt", prompts); // vLLM支持批量prompt
request.put("max_tokens", config.getMaxTokens());
VllmBatchResponse response = webClient.post()
.uri(vllmBaseUrl + "/v1/completions")
.bodyValue(request)
.retrieve()
.bodyToMono(VllmBatchResponse.class)
.block(Duration.ofMinutes(2));
return response.getChoices().stream()
.map(choice -> choice.getText())
.collect(Collectors.toList());
}
}分页注意力(PagedAttention)与KV Cache管理
vLLM的另一个核心创新是PagedAttention——把KV Cache(Attention的Key-Value缓存)像操作系统的虚拟内存一样分页管理,大幅减少KV Cache的内存碎片,让GPU显存利用率从原来的40%提升到90%以上。
从Java工程角度,你需要合理设置KV Cache相关参数:
# vLLM启动配置(在Python侧配置,Java通过API调用)
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--tensor-parallel-size 1 \ # 单GPU
--gpu-memory-utilization 0.90 \ # GPU显存利用率上限
--max-model-len 8192 \ # 最大序列长度
--max-num-seqs 256 \ # 最大并发序列数
--block-size 16 \ # KV Cache块大小(tokens)
--swap-space 10 \ # CPU swap空间(GB),用于KV Cache overflow
--port 8000多GPU策略:张量并行 vs 流水线并行
当单个GPU放不下模型时,需要多GPU:
张量并行(Tensor Parallelism):把一层的矩阵乘法拆分到多GPU并行计算,延迟低但通信开销大,适合同机多GPU。
流水线并行(Pipeline Parallelism):不同的层放在不同GPU,像流水线一样依次处理,通信量小,适合跨机器多GPU,但延迟较高。
# 4GPU张量并行(同一台机器)
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-72B-Instruct \
--tensor-parallel-size 4 # 4个GPU做张量并行
# 2机8GPU流水线并行
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-72B-Instruct \
--tensor-parallel-size 4 \ # 每机4卡张量并行
--pipeline-parallel-size 2 # 2台机流水线并行请求优先级调度
在资源有限时,不是所有请求都一样重要:
@Service
public class PrioritizedRequestQueue {
// 优先级队列:高优先级请求排在前面
private final PriorityBlockingQueue<PrioritizedRequest> queue =
new PriorityBlockingQueue<>(1000,
Comparator.comparingInt(PrioritizedRequest::getPriority).reversed()
);
public void submit(InferenceRequest request) {
int priority = calculatePriority(request);
queue.put(new PrioritizedRequest(request, priority));
}
private int calculatePriority(InferenceRequest request) {
int priority = 0;
// 付费用户优先
if (request.getUserTier() == UserTier.PREMIUM) priority += 100;
if (request.getUserTier() == UserTier.ENTERPRISE) priority += 200;
// 短请求优先(SRPT策略:Shortest Remaining Processing Time)
// 短请求先完成能让更多用户满意
int estimatedTokens = estimateTokens(request.getPrompt());
if (estimatedTokens < 100) priority += 50;
else if (estimatedTokens < 500) priority += 20;
// 实时请求比批处理请求优先
if (request.isRealtime()) priority += 100;
return priority;
}
}GPU利用率监控
没有监控就不知道优化在哪:
@Scheduled(fixedRate = 30000)
public void collectGpuMetrics() {
// 通过vLLM的metrics endpoint获取GPU指标
VllmMetrics metrics = vllmClient.getMetrics();
// 上报到Prometheus
meterRegistry.gauge("gpu.utilization", metrics.getGpuUtilization());
meterRegistry.gauge("gpu.memory.used", metrics.getGpuMemoryUsedMb());
meterRegistry.gauge("vllm.running_requests", metrics.getRunningRequests());
meterRegistry.gauge("vllm.waiting_requests", metrics.getWaitingRequests());
meterRegistry.gauge("vllm.gpu_cache_usage", metrics.getGpuKvCacheUsage());
// 关键指标告警
if (metrics.getWaitingRequests() > 100) {
alertService.sendAlert("推理服务队列积压",
"等待请求数: " + metrics.getWaitingRequests());
}
if (metrics.getGpuUtilization() < 0.3 && metrics.getWaitingRequests() == 0) {
// GPU利用率持续低,考虑缩减实例
log.info("GPU利用率低,可考虑缩减:utilization={}", metrics.getGpuUtilization());
}
}GPU资源是AI自托管场景的最大成本项。通过连续批处理、PagedAttention、合理的并发配置,通常能把GPU利用率从20%提升到60-80%,等效于节省了50-70%的GPU成本。这个收益非常值得投入工程优化的时间。
