作为一名顶级大模型(LLM)部署工程师,我们的核心KPI通常是三个维度的“不可能三角”:低延迟(Low Latency)、高吞吐(High Throughput)和 低成本(Low Cost/High GPU Utilization)

你提到的缓存、批处理、异步并发正是打破这个三角、压榨硬件性能的三把核心“手术刀”。

下面我将结合主流推理框架(如 vLLM, TGI, Triton Inference Server)和实际业务场景(如 RAG、高并发 Chatbot),深入剖析这三者是如何落地的。


一、 缓存策略 (Caching Strategies)

核心目标:用显存换计算,降低 TTFT(首字延迟)。

在 LLM 中,缓存不仅仅是 Redis 里的 Key-Value,更多是指 GPU 显存中的 KV Cache

1. KV Cache 的内存管理(PagedAttention)
  • 痛点:Transformer 的自回归生成过程中,每生成一个 token 都需要复用前面所有 token 的 Key-Value 矩阵。传统做法是预分配连续显存,导致大量显存碎片(Internal Fragmentation),限制了 Batch Size。

  • 落地应用

    • 技术选型:我们会在生产环境使用基于 PagedAttention(如 vLLM)的技术。它像操作系统管理虚拟内存一样,将 KV Cache 切分成块(Block),非连续存储。
    • 效果:显存利用率接近 100%,允许我们在同一张卡上塞入更大的 Batch Size,吞吐量直接翻倍。
2. 前缀缓存 (Prefix Caching / RadixAttention)
  • 场景:在 RAG(检索增强生成)或多轮对话中,System Prompt(系统提示词)或检索到的长文档 Context 往往是不变的,只有用户最后的问题在变。

  • 落地应用

    • 机制:我们会在推理引擎中维护以此前 Prompt Token 序列的 Hash 为键的各种 KV Block 树。
    • 实战:当一个 Request 进来,系统先去“树”上找有没有匹配的前缀。如果 System Prompt + Context 有 2000 token,且命中缓存,GPU 就不需要重新计算这 2000 token 的 KV 值,直接从显存 copy 或映射即可。
    • 收益TTFT(首字延迟)降低 80% 以上,且大幅减少计算负载。
3. 语义缓存 (Semantic Caching)
  • 场景:高频重复查询(如电商客服:“怎么退款?”和“退款流程是什么?”)。

  • 落地应用

    • 在进入 GPU 之前,在网关层(API Gateway)引入向量数据库(Milvus/Qdrant)+ Redis。
    • 将用户 Query 向量化,如果相似度极高(>0.95),直接返回之前的缓存结果,完全不消耗 GPU 资源

二、 批推理 (Continuous Batching / Iteration-level Batching)

核心目标:填满 GPU 计算单元,提升 Throughput(吞吐量)。

传统的 Static Batching(静态批处理)要求“等齐 N 个请求再出发,所有人等最慢的那个人说完才结束”,这在 LLM 场景(输入输出长度不一)是灾难性的。

1. 连续批处理 (Continuous Batching)
  • 原理:也叫 In-flight batching。不再等待整个 Batch 结束。当 Batch 中某个请求生成了 [EOS](结束符),立即将其移出,并从等待队列(Waiting Queue)中插入一个新的请求进来填补空位。

  • 落地应用

    • 调度策略:作为工程师,我需要配置调度器的 max_num_seqs(最大序列数)和 max_num_batched_tokens(最大 token 总数)。
    • 实战:在流量高峰期,GPU 始终处于“满载”状态,没有任何计算气泡(Bubbles)。
    • 收益:相比静态批处理,吞吐量通常提升 10-20 倍
2. 动态 Batch 大小的权衡
  • 实战经验:Batch Size 不是越大越好。

    • Batch 过大 -> KV Cache 占用过多 -> 只能把部分 Layer 卸载到 CPU 或 OOM -> 性能崩塌。
    • Batch 过大 -> 计算密度过高 -> TPOT(每 token 生成时间)增加 -> 用户感觉生成变慢。
    • 策略:通过压测(Locust/K6)找到特定显卡(如 H800 vs 4090)上的最佳甜点(Sweet Spot),通常我们会限制最大 Batch Size 以保证 TPOT 在 50ms 以内。

三、 异步与并发机制 (Async / Concurrency)

核心目标:解耦 I/O 与计算,处理高并发连接,提升用户体验。

LLM 推理是计算密集型,但 Web 服务是 I/O 密集型。

1. 全链路异步 (AsyncIO + FastAPI)
  • 场景:Python 的 GIL 锁限制了多线程能力,但在等待 GPU 推理、等待数据库检索时,CPU 是空闲的。

  • 落地应用

    • 入口层:使用 FastAPI + uvicorn (ASGI)。所有 Controller 全部定义为 async def
    • 推理通信:Web Server 不直接调用 Model,而是通过异步队列(如 Python asyncio.Queue 或 Redis Stream)将请求发送给独立的推理引擎(Engine Process)。
    • 效果:单核 CPU 可以维持数千个 HTTP 连接不阻塞,等待 GPU 返回 token。
2. 流式响应 (Streaming / SSE)
  • 原理:利用 HTTP Server-Sent Events (SSE)。

  • 落地应用

    • Generator 模式:推理引擎内部使用 Python Generator (yield) 吐出 token。
    • 异步迭代:API 层使用 async for 消费这个 Generator,并实时以此 stream 形式推给前端。
    • 收益:用户不需要等整个答案生成完(可能需要 10秒),而是 200ms 内就能看到第一个字,心理等待时间大幅缩短。
3. 分布式推理并发 (Tensor Parallelism)
  • 场景:单卡显存放不下 70B 模型,或者单卡推理太慢。

  • 落地应用

    • 利用 NCCL 库进行多卡通信。我们通常设置 Tensor Parallelism (TP)
    • 并发计算:将矩阵乘法切分到 2 张或 4 张卡上同时计算,然后同步结果。
    • 注意:TP 会带来通信开销,所以作为部署工程师,我会尽量把 TP 限制在单机内部(NVLink 连接),避免跨机 TP。

四、 综合实战:构建一个企业级 RAG 系统

如果我将上述三者结合,搭建一个处理百万级文档的问答系统,架构是这样的:

  1. 请求入口 (Async): FastAPI 接收 HTTP 请求,await 挂起,将 Task 丢入队列。

  2. 语义缓存层: 检查 Redis,如果有高相似度问答,直接返回(Cache Hit)。

  3. 调度层 (Batching):

    • 推理引擎(如 vLLM 实例)从队列拉取请求。
    • 利用 Continuous Batching,将新请求强行插入正在计算的 Batch 中。
  4. 显存管理 (KV Cache):

    • 系统识别到该请求带有一个巨大的公司手册(System Prompt)。
    • 利用 Prefix Caching,发现这本手册的 KV Cache 已经在显存里了,跳过预计算(Prefill),直接开始生成(Decode)。
  5. 流式输出:

    • 每生成一个 token,通过异步队列回传给 API 层。
    • API 层通过 SSE 实时推送到用户浏览器。

总结

  • 缓存 是为了省显存跳过重复计算
  • Batching 是为了在单位时间内处理更多请求
  • 异步 是为了让 CPU 在等待 GPU 时不闲着,同时支持流式体验

一、 什么是 Transformer 的“自回归生成”(Autoregressive Generation)?

一句话解释:就像你在手机上打字,输入法会根据你打的前一个字,猜下一个字。生成了一个字,再把它当作已知条件,去猜下下个字。

流程演示

假设你要模型完成:“床前明月” 后面是什么。

  1. 第一步

    • 输入[床, 前, 明, 月]
    • 计算:模型通过复杂的矩阵运算,算出概率最高的下一个字是 [光]
    • 输出
  2. 第二步(关键点来了,这就叫自回归):

    • 新的输入:把刚才生成的加进去,变成 [床, 前, 明, 月, 光]
    • 计算:模型再次计算。
    • 输出
  3. 第三步

    • 新的输入[床, 前, 明, 月, 光, 疑]
    • 输出
    • …直到生成结束符(EOS)。

为什么这很重要?
因为每生成一个字,都要把整个模型跑一遍。如果模型有 700 亿个参数(70B),生成 100 个字,就要把这 700 亿个参数读写计算 100 次。这就是为什么推理很慢、很吃显存带宽的原因。


二、 前缀缓存 vs 语义缓存:举例说明

我们要区分两种完全不同层面的缓存。

1. 语义缓存 (Semantic Caching) —— “聪明的图书管理员”

场景:它发生在模型计算之前,用来省去整个推理过程。

  • 例子

    • 用户 A 问:“苹果手机怎么截屏?”

    • 模型回答:“按住电源键和音量上键。”(系统把这个问题和答案存进数据库)。

    • 5分钟后,用户 B 问:“iPhone 如何截图?”

    • 流程

      1. 系统把用户 B 的问题变成向量(一串数字)。
      2. 去数据库比对,发现“iPhone 如何截图”和“苹果手机怎么截屏”相似度 99%。
      3. 直接把用户 A 的答案扔给用户 B
    • 结果:GPU 完全没动,耗时 0.01秒,成本为 0。

2. 前缀缓存 (Prefix Caching) —— “考试作弊小抄”

场景:它发生在GPU 显存内部。用于必须得推理,但部分输入内容重复的情况。

  • 例子:我们要搭建一个“公司规章制度问答机器人”。规章制度有 5000 字(这部分叫 Context/Prefix),每次问答都要带上。

    • 用户 A 问[5000字规章] + [请假流程是什么?]

      • GPU 动作:GPU 辛苦地计算了这 5000字规章 的 KV(键值)矩阵,存入显存,然后生成了答案。
      • 缓存动作:系统把这 5000字规章 计算出的中间结果(KV Cache)留在显存里,不释放。
    • 用户 B 问[5000字规章] + [报销怎么走?]

      • 没有缓存时:GPU 必须重新计算这 5000 字,耗时 200ms,然后再算用户的问题。
      • 有前缀缓存时:系统发现这 5000 字的 hash 值和刚才一样。直接复用刚才留在显存里的中间结果。GPU 只需要计算[报销怎么走?]这几个字。
    • 结果:首字生成延迟(TTFT)大幅降低,仿佛那 5000 字不存在一样。


三、 连续批处理 (Continuous Batching) 是自动的吗?

是自动的,但需要推理引擎(如 vLLM)支持并在启动时配置。

1. 在哪里配置?

通常在启动推理服务的命令行参数里。
例如 vLLM 的启动命令:

python -m vllm.entrypoints.api_server \
  --model /path/to/llama3 \
  --max-num-seqs 256 \   <-- 最大同时处理多少个请求
  --max-num-batched-tokens 4096  <-- 一次计算最大吃多少 Token
2. 流程举例:公交车类比

传统批处理 (Static Batching)

  • 公交车必须坐满 4 个人才发车。
  • 乘客 A、B、C、D 上车。
  • A 去 1 站(短问题),B 去 10 站(长问题)。
  • A 到站了,但他不能下车,必须在车上等到 B 坐完 10 站,大家才能一起结束。显存被浪费,A 在傻等。

连续批处理 (Continuous Batching)

  • 公交车(GPU)一直在跑。
  • A、B、C 上车。
  • 第 1 轮循环:大家各生成 1 个字。
  • 第 2 轮循环:大家各生成 1 个字。A 的回答生成完了([EOS])。
  • 关键点:A 立即下车(释放显存)。就在这一瞬间,系统自动把还在排队的 D 拉上车,填补 A 的空位。
  • 第 3 轮循环:现在车上是 B、C、D 一起生成。

这个过程是引擎内部的 Scheduler(调度器)自动完成的,不需要你写代码去控制每一个 token 怎么批处理。


四、 Triton 的动态批处理 (Dynamic Batching)

这和上面的“连续批处理”经常被搞混,但它们处于不同层级。Triton 的动态批处理主要是凑单

1. 怎么回事?

Triton Server 就像一个码头调度员。请求是一个个零散发过来的。如果每来一个请求就发一艘船(启动一次 GPU 计算),太浪费油了。
Triton 的策略:设置一个极其短暂的“等待窗口”,把这段时间里进来的请求打包成一个大包裹(Batch),然后一起发给模型。

2. 配置依据与例子

在 Triton 的配置文件 config.pbtxt 中设置:

dynamic_batching {
    preferred_batch_size: [ 4, 8 ]  # 也就是尽量凑齐 4 或 8 个再发车
    max_queue_delay_microseconds: 5000 # 最多等 5 毫秒
}
  • 流程演示

    1. 0ms:请求 A 到达。Triton 不马上处理,开始计时。
    2. 2ms:请求 B 到达。Triton 放入暂存区。
    3. 4ms:请求 C 到达。Triton 放入暂存区。
    4. 5ms:时间到了(max_queue_delay)。
    5. 动作:Triton 把 A、B、C 打包成一个 Batch Size = 3 的张量,发送给 GPU 模型去跑。
3. 权衡 (Trade-off)
  • max_queue_delay 设置太大(比如 100ms):凑的单多了,GPU 吞吐高了,但是用户觉得卡顿(因为每个请求都白白等了 100ms 也没干活)。
  • max_queue_delay 设置太小(比如 0ms):来一个处理一个,延迟低,但并发高的时候 GPU 处理不过来,排队更久。

注意:现在的 LLM 引擎(如 vLLM)通常自己内部实现了更高级的 Continuous Batching,所以我们在 Triton 部署 LLM 时,往往利用 Triton 做转发,而把 Batching 逻辑交给后端的 vLLM 引擎处理,或者两者配合。


五、 计算密集型 (Compute Bound) vs I/O 密集型 (I/O Bound)

这是计算机科学的基础概念,决定了我们怎么写代码。

1. 计算密集型 (LLM 推理) —— “像做高数题”
  • 场景:显卡(GPU)在做矩阵乘法。
  • 特征:CPU/GPU 占用率 100%,风扇狂转。这时候速度瓶颈在于算力够不够强
  • 状态:就像一个数学教授在黑板上疯狂解题,你问他话他没空理你,因为脑子(算力)占满了。
2. I/O 密集型 (Web 服务) —— “像当餐厅服务员”
  • 场景:FastAPI 接收用户请求,去数据库查数据,或者把请求发给 GPU 等结果。
  • 特征:CPU 占用率很低(可能才 1%)。
  • 状态:就像服务员。服务员把菜单给厨师(GPU)后,大部分时间是在(等厨师做完菜,等客人吃完)。
  • 瓶颈:在于网络传输速度、硬盘读写速度,而不在于脑子算得快不快。

六、 全链路异步 (AsyncIO) 与 流式响应 (Streaming)

因为 Web 服务是 I/O 密集型(大部分时间在等 GPU 结果),所以我们不能让服务员(CPU 线程)傻等。

1. 异步 (AsyncIO) —— “不傻等的服务员”
  • 同步 (Sync) 模式

    • 服务员(线程)把菜单给厨师。
    • 服务员站在厨房门口一动不动,盯着厨师炒菜,炒了 10 分钟。
    • 此时别的客人进店,没人接待。
    • 结果:并发极低,用户体验差。
  • 异步 (Async) 模式

    • 代码里写了 await model.generate()
    • 服务员把菜单给厨师,说:“好了叫我”。
    • 服务员立刻转身去接待下一桌客人
    • 结果:一个服务员(单核 CPU)就能同时接待几千个客人。
2. 流式响应 (Streaming / SSE) —— “挤牙膏”

如果不用流式,用户问一个问题,模型生成 500 个字需要 10 秒。

  • 普通响应:用户看着空白屏幕转圈圈,等了 10 秒,突然 500 字全部弹出来。用户体验:这网好卡,是不是死机了?

  • 流式响应 (SSE)

    • 模型生成第 1 个字“我”。服务器立刻通过 HTTP 把“我”发给前端。
    • 模型生成第 2 个字“是”。服务器立刻发“是”。
    • 用户体验:200 毫秒就看到了第一个字,看着文字一个个蹦出来,感觉响应很快,很有“人”在打字的感觉
总结
  • 自回归:一个字一个字往后猜。
  • 前缀缓存:RAG 场景下,复用固定开头(文档)的计算结果。
  • 语义缓存:有人问过类似问题,直接抄答案,不动 GPU。
  • 连续批处理:有人下车,立马有人上车,不留空座。
  • Triton 动态批处理:在车站先攒一波人,再一起发车。
  • 异步+流式:服务员不傻等厨师,菜做好一点就先给客人端一点,别等全做好了再端。

要理解 Triton 和 vLLM 是如何协作的,以及为什么我们说 vLLM 的 Continuous Batching 更“高级”,我们需要深入到推理服务的内部管道中去。


第一部分:Triton + vLLM 的协作流程 (Triton 做转发,vLLM 做调度)

在现代 LLM 部署架构中,Triton Inference Server 通常扮演“外壳”和“协议转换器”的角色,而 vLLM 是“内核”

1. 为什么要配合?而不是只用 vLLM?
  • Triton 的强项:它支持标准的 HTTP/gRPC 协议,有成熟的指标监控(Prometheus metrics),能同时管理多个模型,支持模型版本控制,且能很好地集成到 K8s 中。
  • vLLM 的强项:它专门针对 Transformer 结构优化,有 PagedAttention 和 Continuous Batching,推理速度极快。
  • 配合策略:我们使用 Triton 的 Python Backend。简单说,就是写一个 Python 脚本嵌在 Triton 里,这个脚本内部调用 vLLM 的库。
2. Triton 把 Batching 交给 vLLM 的具体流程

这里有一个反直觉的操作:我们在 Triton 层通常会把 Dynamic Batching 关闭(或设为无效),让请求透传过去。

场景模拟:
假设配置:Triton (Python Backend) + vLLM Engine。

  • Step 1: 请求到达 Triton (HTTP/gRPC)

    • 用户发来 3 个并发请求:R1, R2, R3。
    • Triton 层配置:我们开启 Triton 的 dynamic_batching 功能。
    • Triton 的动作:Triton 接收到 R1,立刻通过 Python Backend 的 execute 函数传给内部的 vLLM 实例,不等待 R2, R3。
  • Step 2: vLLM 接管 (Async Engine)

    • Triton 的 Python 脚本调用 vllm_engine.add_request(R1)
    • 此时,R1 并不是马上进 GPU,而是进入了 vLLM 内部的一个 Waiting Queue(等待队列)
    • 毫秒级之后,R2, R3 也通过同样方式进入 Waiting Queue。
  • Step 3: vLLM 的调度器 (Scheduler) 工作

    • vLLM 的调度器每隔极短的时间(比如每生成一个 token 的间隙)查看显存状态。
    • 判断:现在显存还够不够塞入 R1, R2, R3 的 KV Cache?
    • 决策:够!于是 vLLM 将 R1, R2, R3 一起拉入 Running Queue(运行队列)
  • Step 4: 真正的 Continuous Batching (GPU 计算)

    • vLLM 控制 GPU 进行一次前向传播。
3. 为什么说 vLLM 的 Batching 更“高级”?

Triton 原生的 Dynamic Batching 是 “Request Level(请求级)” 的,而 vLLM 是 “Iteration Level(迭代/Token级)” 的。

  • Triton 的做法 (低级)

    • 设置窗口时间 50ms。
    • 收到请求 A、B。
    • 等到 50ms,打包 A+B 发给模型。
    • 模型跑完 A 和 B 的全过程后,才能接 C
    • 缺点:如果 A 很短,B 很长,A 生成完了显存也占着,必须等 B 跑完。
  • vLLM 的做法 (高级 - Continuous Batching)

    • 时刻 T1:Batch 里有 [A, B]。A 生成第 10 个字,B 生成第 10 个字。
    • 时刻 T2:A 生成了 [EOS](结束)。
    • 时刻 T3 (关键):在下一个瞬间,vLLM 自动把 A 踢出显存,无缝把排队的 C 插进来。
    • 时刻 T4:现在的 Batch 变成了 [B, C]。B 生成第 12 个字,C 生成第 1 个字。
    • 优势:GPU 永远不闲着,没有短板效应。

第二部分:I/O 密集型的瓶颈与解决方案

你说“Web 服务是 I/O 密集型”,但在 LLM 实际项目中,这个 I/O 瓶颈往往比传统 Web 服务更复杂。

1. 实际项目中的 I/O 瓶颈有哪些?

A. 数据库连接池耗尽 (The DB Connection Bottleneck)

  • 现象:系统并发一高,报错 Too many connections 或者请求卡在连接数据库上。
  • 场景:用户问一个问题,RAG 系统需要去 Vector DB(向量库)查数据,还要去 MySQL 查用户权限,再去 Redis 查历史记录。
  • 原因:每个 HTTP 请求都试图建立一个新的 DB 连接,连接建立握手很慢,且数据库最大连接数有限。

B. 网络带宽与延迟 (The Network Bandwidth/Latency)

  • 现象:TTFT(首字延迟)很低,但用户感觉字出来的速度很慢,或者大段文字传输卡顿。
  • 场景:如果你的服务在 AWS 美国,用户在中国。或者用户上传了一个 10MB 的 PDF 文档做分析。
  • 原因:跨地域传输慢;上传下载大文件阻塞了 Worker 线程。

C. 阻塞式代码 (The “Sync” Killer)

  • 现象:CPU 利用率很低,但吞吐量上不去。
  • 场景:代码里写了 requests.post(openai_api) 或者 time.sleep()
  • 原因:Python 的 GIL 锁。如果一个线程在等 HTTP 响应(同步),整个线程就卡死在那里,不能处理其他用户的请求。
2. 怎么解决?(顶级工程师的工具箱)

解决方案 A:全异步非阻塞 (AsyncIO + Aiohttp)
这是解决 I/O 瓶颈的银弹

  • 做法

    • 不用 import requests,改用 import aiohttphttpx.AsyncClient
    • 不用 import pymysql,改用 import asyncpgmotor (MongoDB)。
  • 效果:当代码执行到 await db.query() 时,CPU 瞬间切换去处理下一个用户的 HTTP 请求,而不是傻等数据库返回。单核 CPU 可抗住数万并发。

解决方案 B:连接池 (Connection Pooling)

  • 做法:服务启动时,预先建立 100 个数据库连接放在池子里。
  • 实战:使用 SQLAlchemy (Async) 或 Redis ConnectionPool。请求来了直接从池子里“借”一个连接,用完“还”回去,避免了 TCP 三次握手的开销。

解决方案 C:流式传输与数据压缩

  • 做法

    • SSE (Server-Sent Events):像上面说的,一个字一个字推,避免等待大包 I/O。
    • Protobuf / gRPC:内部服务调用(比如 Web Server 调 Triton)不要用 JSON(文本太大),用 Protobuf(二进制,极小),网络 I/O 开销减少 50% 以上。

解决方案 D:存算分离(针对大文件)

  • 场景:用户上传 50MB PDF。

  • 做法

    • Web Server 不直接接收文件流(这会把 Python 线程堵死)。
    • 前端直接上传到对象存储(S3/OSS)。
    • Web Server 只接收一个 URL s3://bucket/file.pdf
    • 后台异步 Worker 去下载处理。

这一连串问题非常犀利,直击架构设计的核心。我们一个个来拆解,保证让你豁然开朗。


第一部分:Triton 与 vLLM 的深度纠缠

1. 怎么把 Triton 的 Dynamic Batching 关闭?

你提到的“设置成 0”是个误区。在 Triton 的配置文件 config.pbtxt 中,关闭的方法非常简单:直接不写 dynamic_batching { ... } 这一段代码

  • 开启状态

    # config.pbtxt
    # 如果写了下面这一段,就是开启
    dynamic_batching {
      preferred_batch_size: [ 4, 8 ]
      max_queue_delay_microseconds: 100
    }
    
  • 关闭状态
    把上面那段删掉。Triton 默认就是关闭动态批处理的,来一个请求处理一个。

2. Triton, 模型, vLLM 到底是什么关系?

这是一个经典的俄罗斯套娃结构。

  • 最外层:Triton Inference Server (大管家)

    • 身份:它是服务器软件。
    • 职责:负责监听端口(HTTP/gRPC),接收外部请求,统计 QPS,管理显卡健康状态。它不负责具体的矩阵运算,它只负责“分发任务”。
  • 中间层:Python Backend (翻译官)

    • 身份:Triton 的一种运行模式。
    • 职责:Triton 本身是 C++ 写的,但 vLLM 是 Python 库。我们需要一个中间层把 C++ 的请求转成 Python 对象。你会在 Triton 里写一个 model.py 脚本。
  • 最里层:vLLM Engine (最强大脑)

    • 身份:它只是一个 Python 库(类比 import numpy),被导入到了 model.py 里。
    • 职责:它直接控制 GPU 显存(PagedAttention),加载模型权重文件(Llama/Qwen),执行真正的推理计算。

一句话总结:Triton 是开饭店的(负责接待客人),vLLM 是厨房里的主厨(负责做菜)。

3. Triton 动态批处理 vs vLLM 连续批处理:为什么冲突?
  • Triton 的动态批处理 (Dynamic Batching)“堵门策略”

    • Triton 说:“凑齐 4 个人再进厨房!”
    • 结果:哪怕现在厨房空着,只要不够 4 个人,大家都在门口傻等。这增加了延迟。
  • vLLM 的连续批处理 (Continuous Batching)“流水线策略”

    • vLLM 说:“别堵门!来一个进一个!我内部有传送带,能把新来的人插空塞进正在做菜的队伍里。”

最佳实践
我们在 config.pbtxt删掉 dynamic_batching(关闭 Triton 的堵门)。让请求直接穿透 Triton,第一时间到达 vLLM,让 vLLM 发挥它强大的流水线插队能力。


第二部分:I/O 密集型 (I/O Bound) 到底怎么理解?

是的,字面意思就是“Input/Output 密集”,但核心在于 “等待”

生动的例子:
你(CPU)是一个顶级大厨,做菜速度极快(计算能力强)。

  • 计算密集型 (Compute Bound)

    • 你在切土豆丝。你的手(CPU)必须不停地动,满头大汗。你的速度瓶颈在于你的手够不够快。
  • I/O 密集型 (I/O Bound)

    • 你要做番茄炒蛋,但发现番茄还没买
    • 你打电话给超市(发起网络请求),超市说 10 分钟送来。
    • 在这 10 分钟里,你(CPU)是闲着的,你在玩手机,或者发呆。
    • 瓶颈:在于超市送货的速度(网络延迟、磁盘读取速度),而不在于你切菜快不快。

在代码里的体现

  • 读取文件(Disk I/O)
  • 请求 OpenAI API(Network I/O)
  • 查询 MySQL 数据库(Network/Disk I/O)
    这些操作,CPU 占用率极低,但时间全耗在“传输”上了。

第三部分:存算分离与对象存储

你问到了一个非常敏锐的问题:“即使存到对象存储,最后还不是要下载到和计算代码在一起吗?”

答案是肯定的,必须要下载。存算分离的核心价值在于:谁来下载?在什么时候下载?占用了谁的资源?

场景对比:用户要上传一个 1GB 的 PDF 给模型分析

方案 A:不分离(Web Server 硬抗)

  1. 用户 -> Web Server (FastAPI):用户上传 1GB 文件。
  2. Web Server:Python 线程必须一直维持着这个连接,接收数据流,把数据写到本地硬盘。假设网速慢,传了 2 分钟。
  3. 后果:在这 2 分钟里,Web Server 的这个线程被锁死了,它不能处理其他人的“你好”、“在吗”这种简单请求。如果并发高,Web Server 直接卡死。
  4. Web Server -> GPU:读取本地文件给 GPU。

方案 B:存算分离(正解)

  1. 用户 -> 对象存储 (S3/OSS)

    • Web Server 给用户一个“上传凭证”(Presigned URL)。
    • 用户直接把 1GB 文件传给阿里云/AWS 的 S3 服务器。
    • 关键点流量完全绕过了你的 Web Server。你的 Web Server 甚至不知道用户在传文件,毫不知情,一身轻松。
  2. 用户 -> Web Server

    • 文件传完后,用户只发给 Web Server 一个字符串:"url": "s3://bucket/file.pdf"
    • Web Server 处理这个请求只需要 1 毫秒。
  3. Web Server -> 异步 Worker (GPU 机器)

    • Web Server 把这个 URL 扔到任务队列里。
  4. 异步 Worker -> 下载

    • 专门负责推理的机器(Worker)从 S3 把文件拉下来。
    • 为什么要这样? 因为 Worker 是后台进程,它下载文件慢一点无所谓,不会阻塞面向用户的前端接口。这就叫“解耦”。

总结
存算分离不是为了“不下载”,而是为了不让下载大文件这个动作阻塞了核心的 Web 服务端口,让流量走专用通道(S3),保持 API 的极速响应。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐