LLM 推理服务部署与显存管理完全指南:从一次"4090 单卡跑 7B 模型并发 4 个就 OOM"看懂为什么 transformers 远远不够

2024 年我所在的团队接到一个任务把一个 7B 的开源大模型私有化部署起来给公司内部团队做代码助手 SQL 翻译文档问答老板说一开始预计 100 个内部用户就够了估个两台 4090 的机器跑跑我先用最熟悉的 HuggingFace transformers 写了一个 FastAPI 服务在单卡上把模型跑起来测了一下单条请求 200 毫秒 token 输出速度 30 token/s 老板看完很满意觉得可以上线了然后一上线一连串问题让我重新认识了 LLM 推理这件事远比我想象的复杂第一种最先把我打懵单卡 4090 24GB 显存跑 7B FP16 模型加载完就用掉了 14GB 用户并发一来显存暴涨 4 个用户同时请求显存就 OOM 了服务直接挂第二种最难缠并发 8 个请求时平均延迟从 200ms 涨到 4 秒 GPU 利用率却只有 30% 我盯着 nvidia smi 看了半天才意识到是因为 batching 没做好 8 个请求被串行处理了第三种最离谱我加了 dynamic batching 后吞吐确实涨了可某些请求要生成 2000 token 某些只要 50 token 它们被打成一个 batch 那批 50 token 的用户等了 30 秒才拿到结果因为 batch 要等最慢的那个完成第四种最莫名其妙换成 vLLM 后吞吐瞬间涨了 5 倍我却不知道它做了什么后来才发现是 PagedAttention 把 KV Cache 分页存储显存利用率从 50% 提到了 95% 第五种最致命用户问了一个相同的长 prompt 同样的工具调用模型每次都重新算我才意识到 prompt caching 在私有化部署里也必须做否则成本和延迟都白瞎我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 LLM 推理就是加载模型转发请求返回输出就完事了可这个认知是错的真正能在生产用的 LLM 推理服务是一个显存管理加批处理调度加 KV Cache 加量化加多模型路由的精密系统任何一环做不好都会让 GPU 利用率惨不忍睹或者延迟飙升本文从头梳理 LLM 推理的核心瓶颈在哪 vLLM 和 TGI 各自的优势是什么 continuous batching 是怎么解决传统 batch 的尾部延迟问题 PagedAttention 为什么是显存利用率的革命量化方案怎么选以及一些把推理服务做扎实要避开的工程坑

2024 年我所在的团队接到一个任务把一个 7B 的开源大模型私有化部署起来给公司内部团队做代码助手 SQL 翻译 文档问答 老板说一开始预计 100 个内部用户就够了 估个两台 4090 的机器跑跑。我先用最熟悉的 HuggingFace transformers 写了一个 FastAPI 服务在单卡上把模型跑起来 测了一下单条请求 200 毫秒 token 输出速度 30 token/s 老板看完很满意 觉得可以上线了。然后一上线一连串问题让我重新认识了 LLM 推理这件事远比我想象的复杂。第一种最先把我打懵 单卡 4090 24GB 显存 跑 7B FP16 模型加载完就用掉了 14GB 用户并发一来 显存暴涨 4 个用户同时请求显存就 OOM 了 服务直接挂。第二种最难缠 并发 8 个请求时 平均延迟从 200ms 涨到 4 秒 GPU 利用率却只有 30% 我盯着 nvidia-smi 看了半天才意识到 是因为 batching 没做好 8 个请求被串行处理了。第三种最离谱 我加了 dynamic batching 后吞吐确实涨了 可某些请求要生成 2000 token 某些只要 50 token 它们被打成一个 batch 那批 50 token 的用户等了 30 秒才拿到结果 因为 batch 要等最慢的那个完成。第四种最莫名其妙 换成 vLLM 后吞吐瞬间涨了 5 倍 我却不知道它做了什么 后来才发现是 PagedAttention 把 KV Cache 分页存储 显存利用率从 50% 提到了 95%。第五种最致命 用户问了一个相同的长 prompt 同样的工具调用 模型每次都重新算 我才意识到 prompt caching 在私有化部署里也必须做 否则成本和延迟都白瞎。我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 LLM 推理就是 加载模型 转发请求 返回输出 就完事了 可这个认知是错的真正能在生产用的 LLM 推理服务是一个显存管理加批处理调度加 KV Cache 加量化加多模型路由的精密系统 任何一环做不好都会让 GPU 利用率惨不忍睹或者延迟飙升本文从头梳理 LLM 推理的核心瓶颈在哪 vLLM 和 TGI 各自的优势是什么 continuous batching 是怎么解决传统 batch 的尾部延迟问题 PagedAttention 为什么是显存利用率的革命 量化方案怎么选 以及一些把推理服务做扎实要避开的工程坑

问题背景:为什么 LLM 推理服务比想象中难

很多人对 LLM 推理的认识是 模型 + GPU + API 三件套 实际工程里这种认识能撑过 demo 但撑不过生产。问题的根源在于:

  • LLM 推理是 memory bound 不是 compute bound:7B 模型 FP16 大约 14GB 显存 KV Cache 还能再占数 GB 显存带宽往往是瓶颈而不是算力。
  • 请求长度高度不均衡:prompt 从几十 token 到几万 token output 从几个 token 到几千 token 简单粗暴的 batch 会导致严重的资源浪费和尾部延迟。
  • generation 是迭代的:每生成一个 token 就要重跑一次模型 不像传统 ML 推理一次返回 这让 batching 策略必须重新设计。
  • KV Cache 是显存大户:一个 4k 上下文的请求 KV Cache 可能占 1-2GB 多并发时 KV Cache 的总量比模型本身还大。
  • 不同请求的 SLO 不同:有的请求要求秒级响应 有的可以等几十秒 一刀切的调度策略无法满足。
  • 量化和精度的权衡很微妙:INT8 INT4 GPTQ AWQ 每种方案在显存压缩比 推理速度 精度损失上都有不同表现 选错就翻车。

一 LLM 推理瓶颈分析:为什么 GPU 利用率上不去

在动手优化之前必须先理解 LLM 推理的本质瓶颈。Transformer 推理分两个阶段 prefill 处理 prompt 计算所有 token 的 KV decode 一个 token 一个 token 生成输出每次只算一个新 token 与所有历史的 K V 做注意力。prefill 阶段是 compute bound 因为有大量的并行矩阵乘法。decode 阶段是 memory bound 因为每生成一个 token 都要把整个 KV Cache 从显存里读出来。这就是为什么单请求时 GPU 利用率只有 5-10% 因为 decode 阶段 GPU 算力完全在等显存带宽。

import torch
import time
from dataclasses import dataclass

@dataclass
class InferenceProfile:
    prefill_time_ms: float
    decode_time_per_token_ms: float
    peak_memory_mb: float
    kv_cache_mb: float

def profile_inference(model, tokenizer, prompt: str, max_new_tokens: int = 100) -> InferenceProfile:
    inputs = tokenizer(prompt, return_tensors='pt').to(model.device)
    prompt_len = inputs['input_ids'].shape[1]

    torch.cuda.reset_peak_memory_stats()
    torch.cuda.synchronize()
    t0 = time.time()
    with torch.no_grad():
        out = model(**inputs, use_cache=True)
    torch.cuda.synchronize()
    prefill_ms = (time.time() - t0) * 1000

    past = out.past_key_values
    next_token = out.logits[:, -1].argmax(dim=-1, keepdim=True)

    t0 = time.time()
    for _ in range(max_new_tokens):
        with torch.no_grad():
            out = model(input_ids=next_token, past_key_values=past, use_cache=True)
        past = out.past_key_values
        next_token = out.logits[:, -1].argmax(dim=-1, keepdim=True)
    torch.cuda.synchronize()
    decode_ms = (time.time() - t0) * 1000

    peak_mb = torch.cuda.max_memory_allocated() / 1024 / 1024
    kv_mb = sum(k.element_size() * k.numel() + v.element_size() * v.numel()
                for k, v in past) / 1024 / 1024

    return InferenceProfile(
        prefill_time_ms=prefill_ms,
        decode_time_per_token_ms=decode_ms / max_new_tokens,
        peak_memory_mb=peak_mb,
        kv_cache_mb=kv_mb,
    )

跑一下这个 profile 你就能直观看到 一个 1024 token prompt 200 token output 的请求 prefill 大约 300ms decode 每 token 30-40ms KV Cache 占用大约 500MB。这个 profile 是所有优化的起点 没量化分析就盲目调参 多半是在错误的方向上努力

二 Static Batching vs Dynamic Batching vs Continuous Batching

批处理是提升 GPU 利用率的核心手段。但 LLM 推理的批处理和传统 ML 不一样 因为请求长度差异巨大 生成步数也差异巨大。最早大家用的是 static batching 等到攒满一个 batch 再处理 缺点是延迟高 而且 batch 里最慢的请求会卡住其他请求。后来出现了 dynamic batching 按时间窗口或者 batch 大小触发 灵活了一些。但真正的革命是 continuous batching 也叫 in-flight batching 这是 vLLM TGI 这些现代推理框架的核心。

from dataclasses import dataclass, field
from typing import List, Optional
import time

@dataclass
class Request:
    req_id: str
    prompt_tokens: list
    max_new_tokens: int
    generated_tokens: list = field(default_factory=list)
    finished: bool = False
    arrival_time: float = field(default_factory=time.time)

class ContinuousBatchScheduler:
    def __init__(self, max_batch_size: int = 32, max_total_tokens: int = 8192):
        self.max_batch_size = max_batch_size
        self.max_total_tokens = max_total_tokens
        self.waiting_queue: List[Request] = []
        self.running_batch: List[Request] = []

    def add_request(self, req: Request):
        self.waiting_queue.append(req)

    def step(self, model):
        finished = [r for r in self.running_batch if r.finished]
        self.running_batch = [r for r in self.running_batch if not r.finished]

        while (len(self.running_batch) < self.max_batch_size
               and self.waiting_queue
               and self._total_tokens() + len(self.waiting_queue[0].prompt_tokens) <= self.max_total_tokens):
            self.running_batch.append(self.waiting_queue.pop(0))

        if not self.running_batch:
            return finished

        next_tokens = model.batched_decode(self.running_batch)
        for req, tok in zip(self.running_batch, next_tokens):
            req.generated_tokens.append(tok)
            if tok == model.eos_token_id or len(req.generated_tokens) >= req.max_new_tokens:
                req.finished = True

        return finished

    def _total_tokens(self) -> int:
        return sum(len(r.prompt_tokens) + len(r.generated_tokens) for r in self.running_batch)

这段代码的核心是 step 函数 每次迭代都会:把已完成的请求踢出 batch 把队列里等待的请求加入 batch 然后让模型对当前 batch 的所有活跃请求做一次 forward 生成下一个 token。关键是 完成的请求立刻退出 不需要等其他请求一起完成 新请求随时可以加入 batch 这就把 GPU 利用率从 30% 提到了 85% 以上。这就是 continuous batching 比传统 dynamic batching 强的根本原因。

三 PagedAttention:KV Cache 的虚拟内存革命

在 continuous batching 之外 vLLM 的另一个杀手锏是 PagedAttention。传统的 KV Cache 是为每个请求按最大长度预分配一块连续显存 比如最大 4096 token 即使请求只用了 100 token 也占着 4096 的空间 浪费严重。PagedAttention 借鉴操作系统的虚拟内存思路 把 KV Cache 按页 page 比如每页 16 token 分配 请求按需取页 用多少占多少 显存碎片消失了 利用率从 50% 提到了 95% 同时还能支持 KV Cache 的跨请求共享 比如多个并发请求共享同一段 system prompt 的 KV Cache。

class KVPage:
    def __init__(self, page_id: int, size: int = 16):
        self.page_id = page_id
        self.size = size
        self.used = 0
        self.ref_count = 0

class PagedKVCacheAllocator:
    def __init__(self, total_pages: int, page_size: int = 16):
        self.page_size = page_size
        self.pages = [KVPage(i, page_size) for i in range(total_pages)]
        self.free_list = list(range(total_pages))
        self.block_table = {}

    def allocate(self, req_id: str, num_tokens: int) -> list:
        pages_needed = (num_tokens + self.page_size - 1) // self.page_size
        if len(self.free_list) < pages_needed:
            raise MemoryError(f'KV cache exhausted: need {pages_needed} free {len(self.free_list)}')
        page_ids = [self.free_list.pop(0) for _ in range(pages_needed)]
        for pid in page_ids:
            self.pages[pid].ref_count = 1
            self.pages[pid].used = self.page_size
        self.block_table[req_id] = page_ids
        return page_ids

    def append_token(self, req_id: str):
        pages = self.block_table[req_id]
        last = self.pages[pages[-1]]
        if last.used < last.size:
            last.used += 1
            return pages[-1]
        if not self.free_list:
            raise MemoryError('KV cache exhausted on append')
        new_pid = self.free_list.pop(0)
        self.pages[new_pid].ref_count = 1
        self.pages[new_pid].used = 1
        pages.append(new_pid)
        return new_pid

    def free(self, req_id: str):
        for pid in self.block_table.pop(req_id, []):
            self.pages[pid].ref_count -= 1
            if self.pages[pid].ref_count <= 0:
                self.pages[pid].used = 0
                self.free_list.append(pid)

    def share_pages(self, src_req: str, dst_req: str, num_pages: int):
        src_pages = self.block_table[src_req][:num_pages]
        for pid in src_pages:
            self.pages[pid].ref_count += 1
        self.block_table[dst_req] = list(src_pages)

真正的杀手锏在 share_pages 共享 prefix 的 KV Cache 可以让多个请求共用同一段显存。如果你有 100 个并发请求 它们的 system prompt 都一样 占用 500 token 那只需要分配一份 500 token 的 KV Cache 就够 100 个请求用 节省的显存能用来跑更多并发 这就是 vLLM 在多用户场景吞吐能达到原始 transformers 的 5-20 倍的根本原因。

[mermaid]flowchart TD
A[新请求到达] --> B{KV Cache
空间是否足够}
B -->|不够| C[尝试 preempt
低优先级请求]
C --> D{是否能腾出空间}
D -->|不能| E[放回等待队列]
D -->|能| F[分配 KV pages]
B -->|够| F
F --> G{prompt 前缀
是否能与已有共享}
G -->|能| H[ref count 加 1
复用 pages]
G -->|不能| I[copy 新页]
H --> J[加入运行 batch]
I --> J
J --> K[迭代生成 token]

四 量化:用精度换显存与吞吐

量化是缓解显存压力和提升吞吐的另一把利器。把模型权重从 FP16 压到 INT8 显存减半 推理速度也能提 1.5 倍 INT4 更激进 显存四分之一 但精度损失也更明显。常见的量化方案有 GPTQ AWQ SmoothQuant FP8 它们的核心差异在于 是否需要校准数据 是 weight-only 还是 weight+activation 都量化 精度损失程度等等。

from dataclasses import dataclass

@dataclass
class QuantConfig:
    method: str  # gptq, awq, smoothquant, fp8
    bits: int  # 4 or 8
    group_size: int = 128
    desc_act: bool = False
    sym: bool = True

def estimate_memory(model_params_b: float, quant: QuantConfig) -> dict:
    fp16_bytes = model_params_b * 1e9 * 2
    if quant.method == 'fp16':
        weights = fp16_bytes
    elif quant.method == 'fp8':
        weights = fp16_bytes / 2
    elif quant.method in ('gptq', 'awq') and quant.bits == 4:
        weights = fp16_bytes / 4 + (fp16_bytes / quant.group_size) * 2
    elif quant.bits == 8:
        weights = fp16_bytes / 2
    else:
        weights = fp16_bytes
    return {
        'weights_gb': weights / 1e9,
        'kv_per_token_kb': model_params_b * 0.5,
        'activation_buffer_gb': 1.0,
    }


有了基础估算函数 我们再写一个简单的决策器 把场景参数翻译成具体的量化配置 让生产里能按业务规则自动选型:

def choose_quant(latency_critical: bool, accuracy_critical: bool, vram_gb: float, model_size_b: float):
    if accuracy_critical:
        return QuantConfig(method='fp16', bits=16) if vram_gb >= model_size_b * 2.5 else QuantConfig(method='fp8', bits=8)
    if vram_gb < model_size_b * 0.7:
        return QuantConfig(method='awq', bits=4)
    if latency_critical:
        return QuantConfig(method='fp8', bits=8)
    return QuantConfig(method='gptq', bits=4)

选量化方案要看几个维度。第一是模型类型 LLM 对 INT4 的精度容忍度比图像模型高 一般 4-bit 量化只损失 1-2 个百分点的下游任务准确率 完全可接受。第二是硬件 H100 有原生 FP8 单元 那肯定用 FP8 A100 没有 FP8 那就 INT8 或 INT4。第三是吞吐 vs 延迟 INT4 节省的显存可以让 batch 更大 吞吐更高 但单次推理的 dequantize 操作有开销 单请求延迟略升。我们的经验是 7B-13B 模型用 AWQ INT4 70B 模型用 GPTQ INT4 因为它在大模型上的精度保留更好。

五 模型路由与多实例策略

很多团队上线 LLM 服务时会想 一个模型就够了 后来才发现 不同请求适合不同模型 简单任务用 7B 复杂任务用 70B 代码生成用 CodeLlama 通用对话用 Llama 一刀切的模型部署既贵又不准。所以生产里都会做模型路由 按请求特征分发到不同的模型实例。

from enum import Enum

class TaskType(Enum):
    CHAT = 'chat'
    CODE = 'code'
    SQL = 'sql'
    SUMMARY = 'summary'
    REASONING = 'reasoning'

class ModelRouter:
    def __init__(self):
        self.routes = {
            TaskType.CHAT: 'llama-7b-instruct',
            TaskType.CODE: 'codellama-13b',
            TaskType.SQL: 'sqlcoder-7b',
            TaskType.SUMMARY: 'llama-7b-instruct',
            TaskType.REASONING: 'llama-70b-instruct',
        }
        self.load_thresholds = {
            'llama-7b-instruct': 80,
            'codellama-13b': 80,
            'sqlcoder-7b': 80,
            'llama-70b-instruct': 60,
        }

    def classify(self, prompt: str) -> TaskType:
        text = prompt.lower()
        if any(k in text for k in ('select ', 'insert ', 'update ', 'from ', 'where ')):
            return TaskType.SQL
        if any(k in text for k in ('def ', 'function ', 'class ', '代码', 'code')):
            return TaskType.CODE
        if any(k in text for k in ('总结', '摘要', 'summarize', 'summary')):
            return TaskType.SUMMARY
        if any(k in text for k in ('推理', '为什么', 'why', '分析')):
            return TaskType.REASONING
        return TaskType.CHAT

    def route(self, prompt: str, current_load: dict) -> str:
        task = self.classify(prompt)
        preferred = self.routes[task]
        if current_load.get(preferred, 0) < self.load_thresholds[preferred]:
            return preferred
        fallbacks = {
            TaskType.REASONING: 'llama-7b-instruct',
            TaskType.SQL: 'llama-7b-instruct',
            TaskType.CODE: 'llama-7b-instruct',
        }
        return fallbacks.get(task, 'llama-7b-instruct')

模型路由的设计要点是 主路径加 fallback 主模型过载时降级到通用模型 而不是让请求等待。我们的经验是 70B 模型的并发上限很低 单 A100 80GB 撑不住 20 个并发 必须设个 60% 的水位线 超过就降级到 7B 否则 70B 那条线一阻塞 整个系统延迟雪崩。

六 推理服务的工程坑:那些文档里学不到的

讲完原理来说几个真实踩过的坑。第一个坑是 GPU 显存不光是模型本身 还有 PyTorch 的显存碎片 CUDA 上下文 cuBLAS 工作区 实际可用比理论值少 2-3GB 一定要留余量 否则上线后第一次 OOM 就把你打懵。第二个坑是 vLLM 的 max_num_seqs 参数 它决定并发上限 设太大显存会爆 设太小吞吐上不去 必须结合实际 KV Cache 占用计算 公式大约是 显存 - 模型权重 - 5GB 余量 除以 单请求 KV 平均大小。第三个坑是 多卡部署的 NCCL 配置 张量并行的卡间通信非常敏感 NCCL_P2P_DISABLE NCCL_IB_DISABLE 这些参数没配对 推理速度能差 3-5 倍。

import os
import psutil
import torch

class ServingHealthCheck:
    def __init__(self, port: int, gpu_id: int = 0):
        self.port = port
        self.gpu_id = gpu_id

    def check(self) -> dict:
        gpu_free, gpu_total = torch.cuda.mem_get_info(self.gpu_id)
        gpu_util = (gpu_total - gpu_free) / gpu_total
        cpu_pct = psutil.cpu_percent()
        return {
            'gpu_mem_used_pct': gpu_util * 100,
            'gpu_mem_free_gb': gpu_free / 1024 / 1024 / 1024,
            'cpu_pct': cpu_pct,
            'healthy': gpu_util < 0.95 and cpu_pct < 95,
        }


与健康检查配套的是优雅退出 在做发布或者节点漂移的时候 必须先把正在跑的请求等完 而不是直接 kill 进程 否则用户的请求会被半路截断:

class GracefulShutdown:
    def __init__(self, scheduler: ContinuousBatchScheduler, drain_timeout_sec: int = 60):
        self.scheduler = scheduler
        self.drain_timeout_sec = drain_timeout_sec
        self.shutting_down = False

    def initiate(self):
        self.shutting_down = True

    def can_accept_new(self) -> bool:
        return not self.shutting_down

    def drain_and_exit(self, model):
        start = time.time()
        while self.scheduler.running_batch or self.scheduler.waiting_queue:
            if time.time() - start > self.drain_timeout_sec:
                break
            self.scheduler.step(model)

第四个坑是 长时间运行的显存泄漏 即使有 GC 即使用 torch.cuda.empty_cache 长时间运行的服务显存还是会缓慢增长 必须每天定时 restart 或者监控显存超阈值自动重启。第五个坑是 没做 graceful shutdown 服务重启时正在生成的请求被强杀 用户体验直接断 必须做请求 drain 让进行中的请求完成或者超时 再下线实例 这一项几乎所有团队最初都漏做

关键概念速查

概念 含义 工程价值
Prefill / Decode 推理的两个阶段 性能特征完全不同
KV Cache 注意力 K V 缓存 显存大户必须管理
Static Batching 等齐再处理 简单但延迟高
Dynamic Batching 窗口触发 灵活但有尾部延迟
Continuous Batching 迭代级调度 vLLM TGI 的核心
PagedAttention KV Cache 分页 显存利用率从 50% 到 95%
Prefix Sharing 共享 prompt KV 多并发同 system 显著省显存
GPTQ / AWQ 4-bit 权重量化 显存四分之一精度小幅损失
FP8 H100 原生 8-bit 新硬件首选
Tensor Parallel 多卡分摊一个层 大模型必备但通信敏感

避坑清单

  1. 显存预算必须留 2-3GB 余量 别按理论值压满 上线后碎片化会让你 OOM。
  2. 单卡部署优先用 vLLM 或 TGI 不要用裸 transformers 否则吞吐差一个数量级。
  3. continuous batching 的 max_num_seqs 要按 KV Cache 实际容量算 不是越大越好。
  4. system prompt 共享 必须用支持 prefix caching 的框架 vLLM 0.3+ TGI 1.4+ 才有。
  5. 多卡部署一定要测 NCCL 配置 不配 P2P 速度能差 3-5 倍。
  6. 量化方案要做精度回归测试 别盲目用 INT4 部分任务掉点会很难看。
  7. 不同模型不同租户不同接口 一定要做路由 别一刀切跑大模型贵且慢。
  8. 长跑服务要做显存监控 显存缓涨是真实存在的 设阈值自动重启。
  9. 必须做 graceful shutdown 别让在生成的请求被强杀 用户体验断崖。
  10. 压测必须用真实分布 不要全用 1k token 测了上线发现 4k token 的请求把系统打挂。

总结

LLM 推理服务这事 很多人的直觉是 加载模型 起个 API 就行 这其实是把 我会调 transformers 和 我能在生产跑稳一个推理服务 混为一谈。模型加载只是冰山一角 真正的复杂度全在水面以下 批处理调度 显存管理 量化策略 模型路由 长跑稳定性 每一项都是一座工程大山 都需要你认真翻越。

从原型到生产 你需要做的事远不止 写一个 generate 接口。你要懂 prefill 和 decode 的性能特征 要选合适的推理框架 要算 KV Cache 的显存预算 要做量化精度回归 要设计多模型路由 要应对显存碎片 要做 graceful shutdown 要做监控告警。每一项单独看都不复杂 但它们叠加在一起 才是一个能稳定服务百用户千用户万用户的 LLM 推理系统。少任何一项 都会在某个真实流量里 把你刚才省下的优化时间 连本带利地还回去 而且通常以 GPU OOM 或者 P99 延迟 30 秒 的形式还。

我经常用一个比喻来理解 LLM 推理服务 它有点像高级餐厅的厨房。模型是大厨 显存是厨房面积 KV Cache 是料理台 batch 是出菜节奏 你不能因为请到了米其林大厨 就以为厨房自动会高效运转 你要安排料理台怎么用 怎么排上菜的顺序 怎么处理同时来的复杂订单 怎么不让某一桌的慢菜把所有人都饿着。厨房管理水平决定了大厨能不能发挥水平 而不是大厨本身。

这套架构最难的地方在于 它的复杂度在 demo 阶段几乎完全暴露不了。你单机跑一个请求觉得 4090 真快 大模型真好 但真正多用户上线你会发现 99% 的复杂度都在 那 1% 的并发 长 prompt 高负载场景里。建议任何想做 LLM 推理服务的团队 上线前一定要做一遍真实流量回放 用真实 prompt 分布 真实并发 真实长度去压一压 千万别只测一两个 case 就上线 那种系统一定会在第一周给你看 服务崩溃 GPU OOM 的灾难现场。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

gRPC 微服务超时与重试工程化完全指南:从一次"下游慢 800ms 上游 5 个服务全部雪崩"看懂为什么加 timeout 远远不够

2026-5-24 14:49:16

技术教程

PostgreSQL 索引与执行计划工程化完全指南:从一次"5 亿行订单表查询走全表扫 8 秒不出"看懂为什么加 B-tree 远远不够

2026-5-24 14:58:21

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索