第2027篇:LLM多实例部署——水平扩展还是垂直扩展的决策框架
2026/4/30大约 6 分钟
第2027篇:LLM多实例部署——水平扩展还是垂直扩展的决策框架
适读人群:需要扩容LLM推理服务的架构师 | 阅读时长:约17分钟 | 核心价值:掌握LLM服务扩容的决策逻辑,避免不必要的硬件投入
老板说:"最近LLM请求量增加了,需要扩容,多买几块GPU。"
这个需求背后有一个隐藏的决策:是买更多台服务器(水平扩展),还是给现有服务器加更多GPU(垂直扩展)?
两种方案成本差不多,但效果可能天壤之别。
两种扩展方式的本质差异
决策的核心是:你现在的瓶颈是什么?
如果瓶颈是并发QPS不够(同时请求太多,需要排队),水平扩展有效。
如果瓶颈是模型能力不够(7B效果不好,需要70B),垂直扩展有效。
诊断当前瓶颈
扩容之前先做诊断,搞清楚真正的瓶颈:
@Service
@RequiredArgsConstructor
@Slf4j
public class LlmBottleneckDiagnostic {
private final RestTemplate restTemplate;
@Value("${vllm.base-url}")
private String vllmBaseUrl;
/**
* 采集关键性能指标,诊断瓶颈类型
*/
@Scheduled(fixedDelay = 60000)
public void diagnose() {
DiagnosticReport report = new DiagnosticReport();
try {
String metricsText = restTemplate.getForObject(
vllmBaseUrl + "/metrics", String.class);
report.gpuMemoryUsage = parseMetric(metricsText, "vllm:gpu_cache_usage_perc");
report.pendingRequests = parseMetric(metricsText, "vllm:num_requests_waiting");
report.runningRequests = parseMetric(metricsText, "vllm:num_requests_running");
report.avgTtft = parseMetric(metricsText, "vllm:time_to_first_token_seconds_sum");
} catch (Exception e) {
log.warn("无法获取vLLM指标: {}", e.getMessage());
return;
}
// 诊断逻辑
String bottleneck = diagnoseBottleneck(report);
String recommendation = getRecommendation(report, bottleneck);
log.info("瓶颈诊断: {} | 建议: {}", bottleneck, recommendation);
}
private String diagnoseBottleneck(DiagnosticReport report) {
// 症状1:GPU显存使用率高,说明KV Cache接近上限
if (report.gpuMemoryUsage > 0.90) {
return "KV_CACHE_EXHAUSTED";
}
// 症状2:等待队列长,说明吞吐量不够
if (report.pendingRequests > 50) {
return "THROUGHPUT_BOTTLENECK";
}
// 症状3:正在运行的请求数少但延迟高,说明单个请求处理慢
if (report.runningRequests < 5 && report.avgTtft > 3.0) {
return "SINGLE_REQUEST_LATENCY";
}
return "NORMAL";
}
private String getRecommendation(DiagnosticReport report, String bottleneck) {
return switch (bottleneck) {
case "KV_CACHE_EXHAUSTED" ->
"显存不足,考虑:1)降低max-model-len 2)量化模型 3)升级到更大显存GPU";
case "THROUGHPUT_BOTTLENECK" ->
"吞吐不足,考虑:1)水平扩展(增加实例数)2)增大max-num-batched-tokens";
case "SINGLE_REQUEST_LATENCY" ->
"单请求慢,考虑:1)检查是否开启Flash Attention 2)检查模型量化 3)升级GPU";
default -> "暂无瓶颈,系统运行正常";
};
}
@Data
private static class DiagnosticReport {
double gpuMemoryUsage;
double pendingRequests;
double runningRequests;
double avgTtft;
}
private double parseMetric(String metricsText, String metricName) {
for (String line : metricsText.split("\n")) {
if (line.startsWith(metricName + " ") || line.startsWith(metricName + "{")) {
String[] parts = line.trim().split("\\s+");
try {
return Double.parseDouble(parts[parts.length - 1]);
} catch (NumberFormatException e) {
return 0.0;
}
}
}
return 0.0;
}
}多实例水平扩展的负载均衡
确定需要水平扩展后,负载均衡的配置有几个关键点:
# 水平扩展:三台7B推理实例
upstream llm_cluster {
# 最少连接算法,考虑到LLM请求耗时差异大
least_conn;
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
# 连接复用
keepalive 64;
}
server {
listen 80;
location /v1/ {
proxy_pass http://llm_cluster;
proxy_read_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 不重试:LLM请求非幂等
proxy_next_upstream off;
}
}/**
* Java端的多实例健康感知路由
* 比Nginx静态配置更灵活,能感知实例负载
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class LlmClusterRouter {
private final List<LlmInstanceInfo> instances; // 从配置文件读取实例列表
private final RestTemplate restTemplate;
/**
* 选择当前负载最低的实例
*/
public String selectInstance() {
return instances.stream()
.filter(LlmInstanceInfo::isHealthy)
.min(Comparator.comparingDouble(this::getInstanceLoad))
.map(LlmInstanceInfo::getBaseUrl)
.orElseThrow(() -> new NoAvailableInstanceException("所有LLM实例不可用"));
}
private double getInstanceLoad(LlmInstanceInfo instance) {
try {
String metricsText = restTemplate.getForObject(
instance.getBaseUrl() + "/metrics", String.class);
// 用等待队列长度代表负载
return parseMetric(metricsText, "vllm:num_requests_waiting");
} catch (Exception e) {
// 获取指标失败,视为高负载
return Double.MAX_VALUE;
}
}
/**
* 定期检查实例健康状态
*/
@Scheduled(fixedDelay = 15000)
public void healthCheck() {
for (LlmInstanceInfo instance : instances) {
try {
restTemplate.getForEntity(instance.getBaseUrl() + "/health", Void.class);
if (!instance.isHealthy()) {
log.info("LLM实例恢复: {}", instance.getBaseUrl());
instance.setHealthy(true);
}
} catch (Exception e) {
if (instance.isHealthy()) {
log.warn("LLM实例不可用: {}", instance.getBaseUrl());
instance.setHealthy(false);
}
}
}
}
private double parseMetric(String text, String name) {
for (String line : text.split("\n")) {
if (line.startsWith(name)) {
String[] parts = line.trim().split("\\s+");
try {
return Double.parseDouble(parts[parts.length - 1]);
} catch (Exception e) {
return 0.0;
}
}
}
return 0.0;
}
}垂直扩展:多GPU张量并行
需要运行70B模型时,需要多块GPU做张量并行:
# 4块A100的70B模型部署
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2-72B-Instruct \
--tensor-parallel-size 4 \ # 4块GPU张量并行
--gpu-memory-utilization 0.92 \
--max-model-len 8192 \
--enable-prefix-caching \
--port 8080 \
--host 0.0.0.0
# 检查GPU间互联速度(NVLink比PCIe快得多)
nvidia-smi nvlink --status张量并行需要GPU间频繁通信,NVLink互联速度(600GB/s)远超PCIe(64GB/s)。如果你的多GPU服务器没有NVLink,张量并行的效率会大打折扣,这时候用多机流水线并行可能更好。
混合策略:不同模型分工
实际生产中,最常见的是混合策略——小模型处理大量简单请求,大模型处理少量复杂请求:
/**
* 智能路由:根据请求复杂度分配到不同规格的模型
*/
@Service
@RequiredArgsConstructor
public class HybridModelRouter {
@Qualifier("smallModelCluster")
private final LlmClusterRouter smallModelRouter; // 3台×7B,高吞吐
@Qualifier("largeModelSingle")
private final LlmClusterRouter largeModelRouter; // 1台×72B,高质量
private final RequestComplexityClassifier complexityClassifier;
/**
* 根据请求复杂度路由到合适的模型
* 90%的简单请求用小模型,10%的复杂请求用大模型
* 成本节省:约70%
*/
public String route(ChatRequest request) {
RequestComplexity complexity = complexityClassifier.classify(request);
String instanceUrl = switch (complexity) {
case SIMPLE -> {
// 简单问答、格式化任务 → 7B模型足够
yield smallModelRouter.selectInstance();
}
case MEDIUM -> {
// 中等复杂度:根据当前负载动态决定
if (smallModelRouter.getMinLoad() < 20) {
yield smallModelRouter.selectInstance();
} else {
yield largeModelRouter.selectInstance();
}
}
case COMPLEX -> {
// 复杂推理、专业分析 → 必须用大模型
yield largeModelRouter.selectInstance();
}
};
return callModel(instanceUrl, request);
}
}
@Component
public class RequestComplexityClassifier {
public RequestComplexity classify(ChatRequest request) {
String userMessage = request.getUserMessage();
// 规则1:需要深度推理的关键词
if (userMessage.contains("分析") || userMessage.contains("对比") ||
userMessage.contains("评估") || userMessage.length() > 500) {
return RequestComplexity.COMPLEX;
}
// 规则2:格式化或简单查询
if (userMessage.length() < 50 || userMessage.contains("翻译") ||
userMessage.contains("总结一下")) {
return RequestComplexity.SIMPLE;
}
return RequestComplexity.MEDIUM;
}
}扩容决策的量化依据
给出一个简单的量化框架,帮助做扩容决策:
public class ScalingDecisionHelper {
/**
* 根据当前指标给出扩容建议
*/
public static ScalingRecommendation recommend(
double currentQps,
double targetQps,
double p99LatencyMs,
double latencySlaMs,
String currentModelSize) {
double capacityGap = targetQps / currentQps; // 需要多少倍的容量
boolean latencyOk = p99LatencyMs < latencySlaMs;
if (latencyOk && capacityGap <= 1.0) {
return ScalingRecommendation.noAction("当前容量足够");
}
if (!latencyOk && capacityGap <= 1.0) {
// 流量不大但延迟高:单请求处理慢,可能是模型质量问题或硬件问题
return ScalingRecommendation.optimize(
"延迟超标但流量不大,优先排查:Flash Attention是否启用、GPU是否满负荷、模型是否需要量化");
}
if (latencyOk && capacityGap > 1.0) {
// 延迟正常但吞吐不够:水平扩展
int additionalInstances = (int) Math.ceil(capacityGap) - 1;
return ScalingRecommendation.horizontal(
String.format("建议增加%d台同规格推理实例", additionalInstances));
}
// 延迟超标且吞吐不够:可能需要更大模型或更多资源
if ("7B".equals(currentModelSize)) {
return ScalingRecommendation.hybrid(
"建议:1)水平扩展增加7B实例数量 2)对复杂请求引入72B模型");
}
return ScalingRecommendation.vertical("建议升级到更高规格GPU");
}
}扩容不是越多越好,也不是越贵越好。诊断清楚瓶颈,选对扩展方向,才能把GPU投入转化为真实的服务能力提升。
在LLM场景里,水平扩展解决吞吐问题,垂直扩展解决模型能力问题。这两个问题的解法完全不同,混淆了方向,钱花了也没用。
