大模型推理阶段

一、整体架构:九大阶段

vLLM V1 的推理生命周期分为 9 个核心阶段

 
阶段核心模块职责
① HTTP入口 FastAPI Route 接收请求、参数校验、负载感知
② 分词/预处理 OnlineRenderer + Tokenizer 文本 → Token IDs
③ 请求入队 InputProcessor 封装为 EngineCoreRequest
④ 调度决策 Scheduler 选请求、分配 KV Cache
⑤ Prefill GPUModelRunner 一次性处理所有输入 Token
⑥ Decode循环 GPUModelRunner + Scheduler 逐 Token 生成 + 连续批处理
⑦ 采样 Sampler 从 logits 中选取下一个 Token
⑧ 流式输出 OutputProcessor + Detokenizer Token → 文本,SSE 推送
⑨ 请求完成 Scheduler + OutputProcessor EOS 检测,释放资源

二、双进程架构(面试亮点)

vLLM V1 采用 前端进程 + 后端进程 的双进程设计,通过 ZMQ 进行跨进程通信

  • 前端进程(AsyncLLM):负责 HTTP 接入、请求构造、输出处理

  • 后端进程(EngineCore):负责调度决策、模型执行

这种架构将 I/O 密集型工作(前端)与计算密集型工作(后端)隔离,避免相互阻塞


三、各阶段关键技术点

① HTTP入口

  • 使用 FastAPI 提供 OpenAI 兼容 API

  • load_aware_call 装饰器在高负载时返回 503

  • 流式/非流式自动分流

② 分词与预处理

  • vLLM V1 引入 Renderer 架构,统一处理分词和多模态

  • 若客户端直接传入 prompt_token_ids,可跳过分词,减少约 5-10ms 延迟

③ 请求入队

  • EngineCoreRequest 使用 msgspec.Struct,序列化极快,适合 ZMQ 跨进程通信

  • 请求同时注册到前端的 OutputProcessor 和后端的 EngineCore

④ 调度决策(核心)

Scheduler 维护 waiting / running / prefill 三类队列,调度约束条件包括

  1. KV Cache 容量:可用 GPU 块数

  2. Token 预算max_num_scheduled_tokens

  3. 请求数上限max_num_running_reqs

  4. 策略优先级:FCFS 或优先级调度

max_num_scheduled_tokens 是吞吐和延迟的"旋钮"——值越大吞吐越高但延迟也越高

⑤ Prefill 阶段

  • 所有输入 Token 在 一次 forward pass 中完成

  • 计算结果写入 PagedAttention 的 KV Cache

  • 首 Token 延迟 (TTFT) 与输入长度成正比

  • vLLM V1 支持 Chunked Prefill,长输入可拆分并与 Decode 请求交错执行

⑥ Decode 循环(面试高频)

  • 连续批处理 (Continuous Batching):每个 step 动态组合 Prefill 和 Decode 请求

  • 请求完成立即移除,新请求立即加入

  • 所有请求共享同一 KV Cache 块池

  • 关键指标:TPOT(Time Per Output Token)

⑦ 采样

采样步骤按顺序执行

  1. logits 转 float32

  2. 应用 allowed token ids 白名单

  3. 应用 bad words 排除

  4. 应用惩罚(repetition / frequency / presence)

  5. 采样(greedy 或 random,含 temperature / top_k / top_p)

性能亮点TopKTopPSampler 使用 fused CUDA kernel,将 top-k 和 top-p 合并为一次操作;greedy 采样(temperature=0)几乎是零开销的 argmax

⑧ 流式输出

  • 使用 IncrementalDetokenizer 实现增量反分词——每收到一个新 Token,只解码增量部分

  • 需处理 UTF-8 边界问题:中文字符可能由多个 Token 组成,不完整字节需暂存

  • 最终通过 SSE (Server-Sent Events) 格式推送给客户端

  • stream_interval 可调,默认每 Token 推送

⑨ 请求完成

完成条件包括:EOS Token、达到 max_tokens、Stop String、客户端中断、重复检测
资源回收:释放 KV Cache 块、从 requests dict 移除、清理 RequestState


四、面试可展开的加分点

  1. PagedAttention:vLLM 的核心创新,将 KV Cache 分页管理,类似操作系统虚拟内存,显著提高显存利用率

  2. 连续批处理 vs 静态批处理:传统框架需等所有请求完成再组新 batch,vLLM 每 step 动态调整

  3. Prefill vs Decode 的计算差异:Prefill 是计算密集型(一次性处理所有输入 Token),Decode 是内存带宽密集型(逐 Token 生成,受 KV Cache 读取限制)

  4. vLLM V1 相比 V0 的改进:更简洁的双进程架构、更高效的调度器     

 

为什么要采样:

先给你一个最直白的比喻:模型前向计算(Forward)像是在考试时“算出每道题的分数(Logits)”,而采样(Sampling)则是根据这些分数“决定最终选哪个答案(Token)”。

如果去掉采样直接取最高分(贪心解码),模型就变成了“没有感情的复读机”。具体来说,采样这一步在 vLLM 推理链路中承载了三大核心使命

1. 数学桥梁:将“分数”翻译成“汉字”

模型最后一层输出的 Logits 只是一堆浮点数(比如 -2.3, 5.1, 0.4...),范围不固定。采样第一步就是通过 Softmax 将这些分数归一化成概率分布(所有概率加起来等于 1)。只有完成了这一步转化,才能从这个分布中抽取出一个具体的 Token ID,进而解码成人类能读懂的文本。

2. 创造力调控(面试重点):平衡“确定”与“随机”

这是采样最核心的设计意图。通过调控 Temperature(温度系数)、Top-K 和 Top-P

  • 低温度(如 0.1):拉大高分和低分的差距,采样近乎贪心,输出严谨、死板,适合数学推理/代码生成。

  • 高温度(如 0.9):拉平概率分布,低分词汇也有机会“中奖”,输出更具多样性和创造力,适合写诗/头脑风暴。

  • Top-K/P 截断:强行把累加概率超过 0.9 之外的“稀奇古怪”的词汇剔除掉,防止模型在重要场合胡言乱语。

3. 复杂约束的执行器(工程亮点)

在 vLLM 的采样器(Sampler)中,这一步不仅是“抽签”,还要强行植入各种业务硬约束

  • 禁止词(Bad Words):直接把对应 Token 的概率置为 -inf,物理上杜绝模型说出敏感词。

  • 重复惩罚(Repetition Penalty):动态降低已出现 Token 的概率,强制模型换词,避免陷入“我我我...”的死循环。

  • 语法/JSON 约束(结构化输出):强制只允许符合特定正则表达式的 Token 被采样,保证输出一定是合法的 JSON 格式。


为什么这一步在 vLLM 面试中特别值得聊?

如果在面试中只回答“选 Token”,那就亏了。你可以顺势带出 vLLM 的性能优化细节

“面试官,这一步虽然看起来只是简单的数学运算,但在 vLLM V1 中,它是一个内存带宽敏感型操作。因为每一次 Decode 迭代(每生成 1 个 Token)都要执行一次采样。

vLLM 之所以将 Top-K、Top-P 和 Softmax 合并成一个 Fused CUDA Kernel,就是为了避免反复读写显存。如果把采样拆成多个独立算子调用,GPU 的 Kernel Launch 开销会极大拖慢 TPOT(Time Per Output Token)。所以,vLLM 把惩罚计算、概率归一化和截断全部融合在显存内部完成,用一次内核调用搞定所有逻辑,这是保证高并发推理吞吐的关键细节。”

 

1. 纠正:“筛选” vs “抽样”(概率游戏)

你说“筛选出最靠谱的那个”,这在工程上叫贪心解码(Greedy Decoding),只是采样的一种特例。

真正的采样(Sampling)不是“选第一名”,而是“按概率抽签”。
如果模型预测“我”是60%,“俺”是30%,“朕”是10%。采样不是永远选“我”,而是让“俺”有30%的概率被选中。采样的目的是引入随机性(多样性),防止模型变成只会说套话的机器人。如果永远选最靠谱的,写诗和故事时会极其死板。

2. 核心问题:生成一个Token,采样一次!(绝不等到最后)

明确回答:Decode 每生成 1 个 Token,就立即采样 1 次。生成 100 个 Token,就采样 100 次。

绝对不可能等全部生成完再采样,原因有二:

  • 原因一(自回归特性):大语言模型是自回归的。下一次预测必须依赖上一次输出的 Token。如果不采样出第 1 个 Token,模型根本不知道第 2 个 Token 的输入上下文是什么,后续计算根本无法进行。

  • 原因二(早停机制):如果采样出来的 Token 恰好是 <EOS>(结束符),vLLM 会立刻终止该请求的 Decode 循环,释放显存。如果等全部生成完再采样,那遇到 <EOS> 时早就浪费了大量算力去生成无意义的后续内容。


3. 在 vLLM 中的实际执行流程(面试硬核细节)

在 vLLM V1 的 GPUModelRunner 中,每 1 次 Forward(前向传播) 之后,紧跟 1 次 Sampler 调用。这是一个紧耦合的循环:

Step 1:Forward(计算 Logits) → Sampler(采样出 Token_1) → 返回给前端
Step 2:把 Token_1 拼接到输入 → Forward(计算 Logits) → Sampler(采样出 Token_2) → 返回给前端
...循环往复,直到命中 max_tokens 或 EOS。

这里有一个面试官极爱问的陷阱题:

“既然每次生成都要采样,那 Prefill(预填充)阶段需要采样吗?”

标准答案:不需要! Prefill 阶段只是把用户输入的 Prompt 一次性灌进去,计算出第一个 KV Cache 和第一个 Logits。Prefill 结束后才会进行第一次采样,产出第一个输出 Token。后续的每一次 Decode 迭代,才会严格遵循“先 Forward 计算,后采样”的节奏。

4. 进阶加分点(vLLM 的批处理采样)

如果此时你再补一句工程优化,面试官会眼前一亮:

“虽然每生成一个 Token 就采样一次,但 vLLM 为了压榨 GPU 性能,并不是单个单个地采样
在 Continuous Batching 中,一个 Step 可能同时处理 32 个请求。vLLM 会把这 32 个请求的 Logits 拼成一个 大矩阵(Batch, Vocab),通过一个 Fused(融合)采样 Kernel,一次性同时算出这 32 个请求各自的下一个 Token。这既保证了‘每生成一步采样一次’的逻辑,又通过矩阵运算掩盖了采样的延迟开销。”

总结一句话:采样的粒度是 “每个请求、每个生成步(Step)”,Prefill 结束后触发第一次,之后每一步紧跟 Forward 触发一次,直至结束。绝不等到全剧终。

 

 

1. “一起算”的前提:必须凑成“批次”(Batch)

vLLM 不会因为来了10个请求就立刻无脑一起算。Scheduler(调度器) 会先检查:

  • 显存里还有没有足够的 KV Cache 块

  • 当前 Step 的 Token 预算max_num_scheduled_tokens)够不够?

如果这10个请求都满足条件,Scheduler 会把它们的输入张量 Padding(填充) 成同一个形状(比如都补齐到相同长度),拼成一个 巨大的矩阵(Batch_Size=10),一次性喂给 GPU 做矩阵乘法。所以,它们在物理显存的计算上是“同时”发生的。

2. “绝对隔离”是真的吗?(面试陷阱)

状态是完全隔离的,但显存是共享的。

  • 状态隔离:每个请求都有自己独立的 Request ID、独立的采样参数(比如请求A用 Temperature=0.7 做随机采样,请求B用 Temperature=0 做贪心解码)、独立的停止条件(A要生成100字,B只生成50字)。

  • 显存共享(PagedAttention):它们的 KV Cache 并不是各自划一块固定大小的显存,而是像操作系统分配内存一样,动态分页。这10个请求的 KV 块在显存里是交错存放的(你占第1、3、5块,我占第2、4、6块)。虽然计算在一起,但逻辑上通过 block_table(块表)严格隔开,互不干扰。

3. 最关键的“不同步”时刻(面试加分项)

你说“同时返回第一个字”,仅在它们“同时进入解码(Decode)阶段”时成立

如果这10个请求的 Prompt(输入提示词)长度差异极大(比如请求1输入10个字,请求2输入1000个字):

  • vLLM V1 支持 Chunked Prefill(分块预填充)。它不会让请求2把请求1拖死。

  • 实际时间线可能是这样的

    • 第1个 Step:GPU 只算请求2的前 512 个输入 Token(Prefill),请求1这步不参与计算。

    • 第2个 Step:GPU 可能混合着算(请求2继续 Prefill 剩余部分 + 请求1 开始 Decode 出第一个字)。

    • 第3个 Step:请求2终于 Prefill 完了,和请求1 汇合,此时才开始“同时”生成各自的第一个字。

结论:它们不一定在“绝对物理时间”的同一毫秒返回第一个字,但一旦它们被 Scheduler 编排进同一个 Decode 批次(Decode Batch),那么在这个 Step 内,它们绝对是一起算、一起产出各自的 Token,并一起通过 SSE 流式推给各自用户的。


面试话术升级版(直接背下来):

“vLLM 不是为每个请求单独起线程,而是采用 全局统一的 Step 驱动。在同一个 Step 中,GPU 的前向传播是同步的,Batch 里的所有请求共享这一次 Kernel 调用。采样器(Sampler)会把 Batch 里的 10 组 Logits 同时进行 Top-K/P 截断和采样,得到 10 个 Token。随后,Output Processor 会把这 10 个 Token 分别绑回各自的 Request 上下文,并放入各自的输出队列。所以,在计算层面它们是强同步的(同进退),在业务逻辑和显存管理层面它们是绝对隔离的(弱耦合)。 这种设计极大地压榨了 GPU 算力,同时避免了单请求故障或超长输入拖垮全局。”
这样回答,逻辑严谨且直击底层原理。

 
posted @ 2026-08-24 09:44  老油条666  阅读(22)  评论(0)    收藏  举报