大模型推理服务部署:从 GPU 资源调度到弹性伸缩的架构设计

一、GPU 资源与推理延迟的矛盾:大模型部署的成本困局

大模型推理服务对 GPU 资源的依赖,是部署架构设计的核心约束。一个 7B 参数的模型,推理时至少需要 16GB 显存;一个 70B 参数的模型,则需要多卡并行,显存需求超过 140GB。GPU 资源昂贵且稀缺,A100 显卡的单卡月租成本在万元以上。

更棘手的是,大模型推理的流量模式与传统 Web 服务截然不同。传统服务的流量通常有规律可循(工作时间高、夜间低),可以通过定时扩缩容应对。大模型服务的流量则更加突发——一次产品发布或营销活动,可能在几分钟内将调用量推高 10 倍。GPU 的扩容速度远慢于 CPU,从拉取镜像到模型加载完成,通常需要 3-5 分钟。这段时间内的流量如何承载?

这个矛盾的核心在于:GPU 资源是固定的,而流量是波动的。解决思路有两条——提升单卡利用率(批处理、量化),以及构建弹性伸缩体系(预测性扩容、CPU 降级)。两条路径各有取舍,需要根据业务特性组合使用。

二、推理服务部署架构:从请求调度到弹性伸缩的完整链路

大模型推理服务的部署架构,核心是构建一层"智能调度层",在 GPU 资源有限的条件下,最大化吞吐量并控制延迟。

flowchart TD
    A[推理请求入口] --> B[请求队列与优先级调度]
    B --> C{GPU 实例是否可用?}
    C -->|可用| D[动态批处理组装]
    C -->|不可用| E{是否允许 CPU 降级?}
    E -->|允许| F[CPU 推理实例处理]
    E -->|不允许| G[请求排队等待]
    D --> H[GPU 推理执行]
    H --> I[结果返回]
    F --> I
    G --> C

    subgraph 弹性伸缩控制面
        J[指标采集:队列深度 / GPU 利用率 / 请求延迟]
        K[扩缩容决策引擎]
        L[GPU 实例池管理]
        J --> K
        K --> L
        L --> C
    end

    subgraph 成本控制面
        M[Token 消耗计量]
        N[成本预算校验]
        O[配额管理]
        M --> N --> O
    end

动态批处理是提升 GPU 利用率的关键技术。单条推理请求无法充分利用 GPU 的并行计算能力,将多条请求合并为一个 Batch 执行,可以显著提升吞吐量。但批处理引入了等待延迟——必须等待足够多的请求到达或等待超时,才能组装一个 Batch。等待时间越长,吞吐量越高,但单条请求的延迟也越高。

弹性伸缩控制面根据实时指标(队列深度、GPU 利用率、P99 延迟)决定是否扩容 GPU 实例。预测性扩容基于历史流量模式,在流量高峰到来前提前扩容,避免冷启动延迟。

三、动态批处理与弹性伸缩的生产级实现

3.1 动态批处理调度器

/**
 * 动态批处理调度器:将多条推理请求合并为一个 Batch 执行
 * 核心参数:最大 Batch 大小、最大等待时间
 * 策略:任一条件满足即触发执行,平衡吞吐量与延迟
 */
@Service
public class DynamicBatchScheduler {

    // 最大 Batch 大小:受 GPU 显存限制
    private final int maxBatchSize;
    // 最大等待时间:超过此时间即使 Batch 未满也执行
    private final long maxWaitTimeMs;

    private final BlockingQueue<InferenceRequest> requestQueue;
    private final InferenceEngine inferenceEngine;

    public DynamicBatchScheduler(InferenceEngine engine, int maxBatchSize,
                                  long maxWaitTimeMs) {
        this.inferenceEngine = engine;
        this.maxBatchSize = maxBatchSize;
        this.maxWaitTimeMs = maxWaitTimeMs;
        this.requestQueue = new LinkedBlockingQueue<>(1000);

        // 启动批处理线程
        Thread.ofPlatform().name("batch-scheduler").start(this::processLoop);
    }

    /**
     * 提交推理请求:异步返回结果
     * 使用 CompletableFuture 实现请求与执行的解耦
     */
    public CompletableFuture<InferenceResult> submit(InferenceRequest request) {
        CompletableFuture<InferenceResult> future = new CompletableFuture<>();
        request.setFuture(future);

        if (!requestQueue.offer(request)) {
            // 队列已满,快速失败
            future.completeExceptionally(
                new RejectedExecutionException("推理队列已满,请稍后重试"));
            return future;
        }
        return future;
    }

    /**
     * 批处理主循环:持续从队列中取请求并组装 Batch
     */
    private void processLoop() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                List<InferenceRequest> batch = new ArrayList<>();

                // 阻塞等待第一个请求
                InferenceRequest first = requestQueue.take();
                batch.add(first);

                // 在等待时间内尽量收集更多请求
                long deadline = System.currentTimeMillis() + maxWaitTimeMs;
                while (batch.size() < maxBatchSize) {
                    long remaining = deadline - System.currentTimeMillis();
                    if (remaining <= 0) break;

                    InferenceRequest req = requestQueue.poll(remaining,
                        TimeUnit.MILLISECONDS);
                    if (req != null) {
                        batch.add(req);
                    }
                }

                // 执行批量推理
                executeBatch(batch);

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    /**
     * 执行批量推理并分发结果
     * 关键设计:单条推理失败不影响整个 Batch
     */
    private void executeBatch(List<InferenceRequest> batch) {
        try {
            List<InferenceResult> results = inferenceEngine.inferenceBatch(batch);

            // 将结果分发到各个请求的 Future
            for (int i = 0; i < batch.size(); i++) {
                batch.get(i).getFuture().complete(results.get(i));
            }
        } catch (Exception e) {
            // 整批失败:逐条标记异常
            for (InferenceRequest req : batch) {
                req.getFuture().completeExceptionally(e);
            }
        }
    }
}

3.2 基于指标驱动的弹性伸缩

/**
 * GPU 推理实例弹性伸缩控制器
 * 核心思路:基于队列深度和 GPU 利用率两个指标决策扩缩容
 * 扩容策略:队列深度超过阈值 → 立即扩容
 * 缩容策略:GPU 利用率持续低于阈值 → 延迟缩容
 */
@Component
public class GpuAutoscaler {

    private final KubernetesClient k8sClient;
    private final MetricsCollector metricsCollector;

    // 扩容阈值:队列中等待的请求数
    private static final int SCALE_UP_QUEUE_THRESHOLD = 50;
    // 缩容阈值:GPU 利用率低于此值持续 5 分钟后缩容
    private static final double SCALE_DOWN_UTILIZATION_THRESHOLD = 0.3;
    // 缩容冷却时间:避免频繁缩容导致的抖动
    private static final Duration SCALE_DOWN_COOLDOWN = Duration.ofMinutes(10);

    private Instant lastScaleDownTime = Instant.MIN;

    /**
     * 定时检查扩缩容需求(每 30 秒执行一次)
     */
    @Scheduled(fixedDelay = 30000)
    public void checkScaling() {
        int currentReplicas = getCurrentReplicas();
        int queueDepth = metricsCollector.getQueueDepth();
        double gpuUtilization = metricsCollector.getAverageGpuUtilization();

        // 扩容判断:队列积压严重
        if (queueDepth > SCALE_UP_QUEUE_THRESHOLD) {
            int targetReplicas = calculateTargetReplicas(
                currentReplicas, queueDepth);
            scaleUp(targetReplicas);
            return;
        }

        // 缩容判断:GPU 利用率持续偏低
        if (gpuUtilization < SCALE_DOWN_UTILIZATION_THRESHOLD
                && currentReplicas > getMinReplicas()
                && Instant.now().isAfter(
                    lastScaleDownTime.plus(SCALE_DOWN_COOLDOWN))) {
            scaleDown(currentReplicas - 1);
            lastScaleDownTime = Instant.now();
        }
    }

    /**
     * 计算目标副本数:基于队列深度线性推算
     * 避免逐个扩容导致的多次冷启动
     */
    private int calculateTargetReplicas(int currentReplicas, int queueDepth) {
        // 每个副本的处理能力约为 20 QPS
        int requiredReplicas = (queueDepth / 20) + currentReplicas;
        // 限制最大副本数,防止失控扩容
        return Math.min(requiredReplicas, getMaxReplicas());
    }

    /**
     * 执行扩容:调用 K8s API 修改 Deployment 副本数
     * 扩容后需要等待新实例就绪(模型加载完成)
     */
    private void scaleUp(int targetReplicas) {
        k8sClient.apps().deployments()
            .inNamespace("llm-serving")
            .withName("inference-server")
            .scale(targetReplicas);

        // 等待新实例就绪
        waitForReady(targetReplicas);
    }

    /**
     * 预测性扩容:基于历史流量模式,在高峰到来前提前扩容
     * 适用于流量模式规律的场景(如工作日白天高峰)
     */
    @Scheduled(cron = "0 0 8 * * MON-FRI")
    public void predictiveScaleUp() {
        int peakReplicas = getHistoricalPeakReplicas();
        if (getCurrentReplicas() < peakReplicas) {
            scaleUp(peakReplicas);
        }
    }
}

弹性伸缩的关键设计点在于"扩容激进、缩容保守"。扩容需要快速响应流量增长,因此采用线性推算一次性扩到目标副本数,避免逐个扩容导致的多次冷启动。缩容则需要谨慎,因为 GPU 实例的启动成本高,频繁缩容再扩容会造成资源浪费。缩容冷却时间(10 分钟)确保了缩容决策的稳定性。

四、GPU 部署的工程代价与替代方案的取舍

GPU 部署方案存在几个需要正视的工程代价。

冷启动延迟。 GPU 实例从启动到模型加载完成,通常需要 3-5 分钟。在此期间,新实例无法处理请求。对于突发流量,这段冷启动时间可能导致请求积压。缓解手段包括:保持最低数量的热备实例、使用模型预热(提前加载模型到显存)、以及 CPU 降级(在 GPU 实例就绪前,由 CPU 实例处理低优先级请求)。

显存碎片化。 GPU 显存不支持按需分配的细粒度管理。一个模型加载后,即使只使用了 60% 的显存,剩余的 40% 也难以被其他模型利用。多模型共享 GPU 需要精确的显存规划,否则会出现"显存总量够但无法加载新模型"的尴尬局面。

量化与精度的取舍。 INT8 量化可以将模型显存占用减半,推理速度提升 2-3 倍,但会引入精度损失。对于分类、检索等对精度不敏感的任务,量化是值得的。对于需要精确输出的场景(如代码生成、数学推理),量化可能导致错误率显著上升。是否量化,需要基于业务场景的精度容忍度来决策。

适用边界建议: 对于 7B 以下的模型,单卡部署即可满足大部分场景,架构复杂度可控。对于 70B 以上的模型,多卡并行部署的复杂度急剧上升,建议使用专业的推理框架(如 vLLM、TGI)而非自建部署方案。对于调用量极低的内部工具,按量付费的 Serverless 推理服务(如 AWS Bedrock、Azure AI)比自建 GPU 集群更经济。

五、总结

大模型推理服务的部署架构,核心是在 GPU 资源有限的条件下,通过动态批处理提升单卡利用率,通过弹性伸缩应对流量波动,通过 CPU 降级保障可用性底线。三个机制协同,才能在成本、延迟和可用性之间找到平衡点。

落地路线上,建议从单卡部署起步,先验证模型推理效果是否满足业务需求;再引入动态批处理,在延迟可接受的前提下提升吞吐量;最后构建弹性伸缩体系,从简单的指标驱动扩缩容开始,逐步引入预测性扩容和 CPU 降级。每个阶段都需要配套成本监控——GPU 利用率、单次推理成本、月度总成本——确保部署架构的优化方向是降低成本而非增加复杂度。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐