news 2026/10/14 14:39:42

vLLM 0.7.3源码解析:PagedAttention与调度器核心机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
vLLM 0.7.3源码解析:PagedAttention与调度器核心机制

简介:VLLM-0.7.3源码包是一套面向大模型推理加速场景的完整工程实现,特别适合具备Python与C++基础的AI框架开发者、算法研究员用于理解高性能推理引擎的架构设计与内部机制,并在此基础上进行针对性优化与二次开发。资源包采用zip打包,共收录1896个文件;其中1186个Python脚本构成主要逻辑,47个CUDA源文件与55个CUDA头文件负责底层算子,178个JSON、45个YAML/yml和40个Shell脚本提供配置与自动化运维支持,132个Markdown文档辅助阅读,整体压缩包约46.33MB。目前已有342人浏览学习,对于源码类资源具备实际参考价值。除了完整源码,包内还包含容器部署、CMake构建、代码规范与工具脚本等工程文件,可帮助快速搭建编译环境;研读attention、quant、torch_bindings等核心模块,能够掌握高速令牌生成、KV Cache管理与内存优化等关键实现思路,为部署企业级大模型服务及性能调优提供直接参考。同时,丰富的文档和配置示例也能加速上手过程,降低从源码阅读到实际部署的难度。

1. VLLM-0.7.3 源码到底在看什么:一个推理引擎的内核解剖

第一次把 VLLM-0.7.3 源码拉下来的人,十有八九会在vllm/目录里迷路。这版代码已经不是早期那个“一两个文件搞定调度”的研究原型,而是被拆成了 API Server、LLM 入口、EngineCore、调度器、模型执行器、Worker 好几层。但反直觉的是,真正决定推理性能、也是这版源码里最值得读的东西,其实只有三块:KV Cache 的物理块管理、迭代级调度循环、以及 attention 后端的派发机制。这次不打算把每个文件都讲一遍,而是顺着这三条主线,带你把 0.7.3 的源码读透。适合两类人:一类是线上服务已经跑起来、但吞吐上不去想知道调度器到底卡在哪的;另一类是准备基于 vLLM 做二次开发、还没找到入口的新手。

2. 从入口到推理:VLLM 0.7.3 的核心模块与启动链路

读源码最忌从头到尾一页页啃。我一般先把目录结构摸清,再决定从哪条链路往里钻,否则很容易在类与类之间的调用关系里绕晕。

# 在源码根目录先看一遍顶层目录结构,心里有个地图 find vllm -maxdepth 2 -type d | sort

输出里你会看到几个关键目录:vllm/entrypoints管对外接口,vllm/engine和vllm/v1是引擎核心,vllm/scheduler放调度器,vllm/model_executor是模型加载与执行,vllm/attention是 attention 后端和算子。0.7.3 里vllm/v1目录已经站在默认路径上,如果你还习惯看老版本llm_engine.py那套结构,需要先把心态切到 V1 的执行思路上来。

2.1 LLM 类与 EngineCore:API 层到执行层的第一次解耦

打开vllm/entrypoints/llm.py,你会看到LLM类。它是对外最友好的离线推理入口,但往里走一步就会发现,这个类只是门面,真正的活都在它背后。在离线场景下,LLM会创建一个引擎实例;在 0.7.3 里,这个引擎被进一步封装成EngineCore。名字很抽象,作用却很实在:它把“接收请求”“调度”“执行模型”三个环节分到不同的执行上下文,避免 GIL 和慢操作互相拖后腿。

# 最小可运行调用(离线推理) from vllm import LLM, SamplingParams llm = LLM( model="/data/models/MyLocal-4B-Instruct", # 本地模型路径,或传仓库里的模型名 tensor_parallel_size=1, # 张量并行卡数 gpu_memory_utilization=0.85, # 给 KV Cache 预留显存比例 max_model_len=8192, # 模型最大上下文长度 ) params = SamplingParams( temperature=0.7, top_p=0.8, max_tokens=512, ) outs = llm.generate(["把这段文本翻译成英文:VLLM 的源码值得读"], params, use_tqdm=False) print(outs[0].outputs[0].text)

逻辑说明:model参数必填,0.7.3 支持本地目录路径或模型仓库里的模型名,底层会走模型仓库的加载逻辑。use_tqdm在离线批量场景建议关掉,进度条本身会拖慢高并发请求的处理节奏。gpu_memory_utilization是 vLLM 最核心的显存分配参数,它在初始化时就决定了 KV Cache 能从显存里分到多少块,后面 PagedAttention 的所有分配都建立在这个预算上。

参数说明:tensor_parallel_size大于 1 时,权重会按张量并行切到多卡,每张卡的 KV Cache 独立分配,总显存预算按卡数叠加。max_model_len设错会出现两种典型情况:设大了 KV Cache 装不下长序列,设小了长文本直接截断,最终生效值源码里会和模型自身的最大上下文长度取最小值。

对于想直接啃源码的人,我建议沿着vllm/v1这个目录走。0.7.3 已经默认走 V1 执行路径,这个目录的结构比老的vllm/engine清晰得多。很多分析文章还停留在老版本的add_request + step写法,思路差不多,但类名和调用链已经变了,对照源码时别被旧资料带偏。

2.2 调度器 Scheduler:谁先算、谁排队、谁被抢占

vLLM 和传统批处理最大的区别是迭代级调度,也叫连续批处理。一个 batch 里的序列不是等全部跑完才换下一批,而是每解码出一个 token,就重新调整 batch 成员。谁生成了结束符,谁立刻退出;谁还在排队,只要显存有余量,就可以插进下一轮迭代。这个决策逻辑全部集中在调度器里。

在 0.7.3 源码里,调度器核心方法是schedule(),它每一轮迭代要回答三个问题:哪些请求可以加入本轮的 running 队列?哪些 running 序列本轮要被抢占?抢占出来的空间给谁用?

# 简化后的调度决策逻辑,对应源码中 schedule() 的主要分支 def schedule(self): # 1) 先把上一轮跑完的序列清出去 for seq in self.running: if seq.is_finished(): self.running.remove(seq) self.block_manager.free(seq) # 释放 KV Cache 物理块 # 2) 在有富余预算的前提下,把等待队列里的请求拉进来 remaining_budget = self.max_num_seqs - len(self.running) for seq in self.waiting_queue: if remaining_budget <= 0: break if self.block_manager.can_allocate(seq): self.running.append(seq) remaining_budget -= 1 else: self.preempt_one(seq) # 显存不足时触发抢占 # 3) 本轮真正送去模型执行的序列 return list(self.running)

逻辑说明:max_num_seqs是硬上限,它与 KV Cache 块数共同决定并发序列数。V1 引擎里这个值还被进一步拆成“最多调度多少序列”和“每轮最多加多少 token”两个维度。can_allocate检查的是剩余物理块是否够给这个序列至少分配一块。如果不够,调度器不会进入等待,而是直接踢人。

抢占不是随便踢,0.7.3 里有明确策略:优先抢占已经生成 token 数最少且本轮还没开始计算的序列,因为重算它们的代价最小。用大白话讲,调度结束后,running 队列这批序列被送去前向,拿到 logits,采样出新 token,更新 KV Cache,然后进入下一轮。

V1 调度器和旧版调度器的关键差别在预算管理。旧版用max_num_batched_tokens做硬截断,V1 把这个值换成了更细的“每轮最多接受的 token 数量”,并且会根据当前空闲 KV 块动态调整。在vllm/v1/scheduler里,它维护了一个 token 预算变量,每轮调度完重新计算。带来的直接效果是:高并发排队时,V1 的吞吐曲线比旧版平稳不少,不会出现一批 32 个请求全部挤进一个 step 然后集体超时的情况。

2.3 PagedAttention 与 KV Cache 的物理块管理

PagedAttention 是 vLLM 的立身之本。核心做法是把 KV Cache 切成固定大小的 block,每个 block 里放固定数量 token 对应的 K 和 V 张量。这样显存不再为每条序列预留整段连续空间,而是按需分配不连续的物理块。0.7.3 的vllm/attention目录里能看到后端派发和算子实现,但真正管分配的逻辑在BlockManager里。

# KV Cache 块分配的简化数据结构 class BlockAllocator: def __init__(self, num_blocks, block_size): self.free_blocks = list(range(num_blocks)) # 空闲块列表 self.block_size = block_size # 每块放的 token 数 def allocate(self, seq_id, num_tokens): blocks = [] # 按需分配,返回块号列表 while num_tokens > 0: block_id = self.free_blocks.pop() blocks.append(block_id) num_tokens -= self.block_size return blocks

逻辑说明:block_size默认是 16,一个 block 里装 16 个 token 位置的 K、V 向量。分配是按需的,新序列只拿第一个块,等生成满 16 个 token 再拿下一个块。这也解释了为什么 vLLM 在长序列场景下特别省显存。物理块不连续没问题,attention 计算时通过块表找到每个 token 在物理块里的位置,这个块表就是每条序列在显存里的“页表”。

0.7.3 源码里还有一块和 PagedAttention 强相关的逻辑:prefix caching。它把共享前缀的 KV Cache 块复用给多条序列,实现上复用了同一个BlockAllocator,只是额外记录引用计数,计数归零才真正释放块。这个机制线上很有用,但也埋着一个容易踩的坑,第 5 章会展开。

attention 后端的选择也是一个值得在源码里看明白的点。0.7.3 启动时会根据硬件能力和环境变量,在 flash-attn、xformers 以及一些降级算子里挑一个实际执行 attention,派发逻辑集中在 attention 目录的 backend 子目录。日志里看到Using FlashAttention Backend说明走的是最快路径,看到 fallback 到某些老算子,一般意味着 CUDA 工具链或 flash-attn 版本不对。这个选择是启动时一次性决定的,值得花十分钟去源码里确认一遍,避免生产环境跑几天才发现用的不是最优后端。

3. 源码读法:一条请求从 submit 到 token 流出的完整路径

3.1 请求进来:LLM.generate 到底做了什么

很多人以为llm.generate()是一次性把整段文本算完。从外部看确实如此,但源码内部它是一次放入、分多轮迭代取回。关键步骤是:先做 tokenize,再add_request把请求送进队列,然后循环调step()直到所有序列结束。

# LLM.generate 的核心流程(逻辑等价于 0.7.3 源码中的实现) def generate(self, prompts, sampling_params): # 1) 为每个 prompt 创建请求对象 for idx, prompt in enumerate(prompts): input_ids = self.tokenizer.encode(prompt) # 先做 tokenize self.engine.add_request( request_id=str(idx), prompt=prompt, prompt_token_ids=input_ids, sampling_params=sampling_params, ) # 2) 循环驱动引擎,直到所有请求完成 outputs = {} while self.engine.has_unfinished_requests(): step_outputs = self.engine.step() # 每一轮迭代 for out in step_outputs: if out.finished: outputs[out.request_id] = out # 3) 按请求原有顺序返回 return [outputs[str(i)] for i in range(len(prompts))]

逻辑说明:add_request并不会立即开始计算,它只是把请求放进引擎内部的待调度队列。step()是每一轮迭代的驱动器,先让调度器选出本轮要算的序列,再跑模型前向、采样、更新状态,然后返回这一步新完成的输出。引擎内部维护一个序列状态机,典型状态是WAITING到RUNNING再到FINISHED_STOP或FINISHED_LENGTH,你从 outputs 里拿到的finished字段就是这个状态机的直观反映。

参数说明:prompts可以传字符串列表,也可以直接传 token id 列表。传 token id 的好处是跳过 tokenizer,在预分词场景能省一点时间,但你要自己对 id 的合法性负责。SamplingParams里的max_tokens决定序列最多走多远,达到上限后在FINISHED_LENGTH状态结束,不是报错。

3.2 调度循环:每一次 step 迭代的决策点

step()是整个引擎里最重要的单点。0.7.3 的 V1 路径下,step()大体按这个顺序执行:同步所有序列状态 → 调度器生成本轮计划 → 校验 KV Cache 分配 → 执行模型前向 → 采样更新 token → 释放完成序列的资源。每一步里都埋着一个值得看的决策点。

# step() 的五个核心决策点,注释对应源码里的关注位置 def step(self): # 决策点1: 哪些序列本轮进入候选列表 scheduled = self.scheduler.schedule() # 对应 2.2 的调度逻辑 # 决策点2: 本轮新增多少新的 KV Cache 块 self.block_manager.allocate_for(scheduled) # 决策点3: 模型前向,真正吃算力的地方 hidden_states = self.model_executor.execute(scheduled) # 决策点4: 采样,从 logits 里选出新 token new_tokens = self.sampler.sample(hidden_states) # 决策点5: 把新 token 追加到序列尾部,更新状态 self.sequence_manager.update(scheduled, new_tokens)

逻辑说明:决策点 1 决定“算谁”,调度器会保证本轮被选中的序列总 token 数不超预算,这是吞吐稳定的底线。决策点 2 决定“显存够不够”,预算不足会触发抢占,V1 里被抢占的请求进入一个 preemption 队列,下一轮调度再处理。决策点 3 是 GPU 算力消耗点,它把输入 token 拼成一个 batch 跑一次前向,GPU 利用率高的时候基本都是卡在这里。决策点 4 的采样看起来轻,实际上在 batch 很大时也不便宜,它最容易成为 CPU 与 GPU 之间的同步等待点。决策点 5 更新每个序列的 token 列表,同时判断是否触发生成结束条件。

日志里的Avg prompt throughput和Avg generation throughput就是在整个循环里按 token 数累计出来的,不是模型日志,是引擎自己统计的。

3.3 输出采样与流式返回:logits 细节

采样决定一个 batch 里每条序列下一步吐出什么 token。vLLM 支持多种采样参数组合:temperature、top_p、top_k、frequency_penalty、presence_penalty等。在 0.7.3 源码里,这些参数最终会在Sampler中被处理成一个统一的 logits 后处理流程。

# Sampler 中 logits 后处理的简化逻辑 def sample(self, hidden_states): # 1) 映射到词表维度得到 logits logits = self.last_hidden_to_logits(hidden_states) # 2) 应用惩罚项 if self.params.presence_penalty != 0.0: logits = apply_presence_penalty(logits, self.history) if self.params.frequency_penalty != 0.0: logits = apply_frequency_penalty(logits, self.history) # 3) 温度缩放 logits = logits / self.params.temperature # 4) top_p / top_k 截断 logits = apply_top_p_top_k(logits, self.params.top_p, self.params.top_k) # 5) softmax 转概率并采样 return sample_from_logits(logits)

逻辑说明:presence_penalty和frequency_penalty都是对历史 token 的惩罚,区别在于前者只惩罚“出现过”的 token,后者按出现次数惩罚,源码里一个加固定值、一个乘次数,调参时不要搞混。temperature在实现上是直接对 logits 做除法,不是对概率做,这俩数学意义不同。apply_top_p_top_k先按概率排序再截断,词表很大、batch 很大时会是热点,0.7.3 里对应算子已经在 GPU 上,但如果词表特别大,采样仍是不可忽略的开销。

流式返回的核心是:引擎每完成一次step(),就把这一步为新生成的 token 立即推给回调,而不是等序列全部结束。在 0.7.3 的异步引擎里,这是靠结果队列实现的,前端流式效果本质是一个异步循环在从队列里取 token。有个容易忽略的细节:首 token 的到达时间不只是模型快慢决定的,它包含了从请求进队到第一轮调度到该序列的全部等待时间。所以流式体验不好时,先去查调度,再查网络。

4. 读源码时最该盯住的参数:从默认值反推设计意图

4.1 max_num_seqs 与 max_num_batched_tokens:吞吐和延迟的平衡点

这两个参数直接决定一个 step 里能塞多少序列、多少 token。max_num_seqs是序列数上限,max_num_batched_tokens是单轮批处理 token 总数上限。它们是一对默契的组合,前者限制并发条数,后者限制计算量。只调大其中一个,收益通常有限,甚至会把另一个变成瓶颈。

# 服务化启动时调整并发与批大小 vllm serve /data/models/MyLocal-4B-Instruct \ --max-num-seqs 64 \ --max-num-batched-tokens 8192 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --enable-prefix-caching

逻辑说明:--max-num-seqs 64允许一张卡上最多同时跑 64 条序列,设大了并发高,但每条序列要分配 KV Cache 块,显存不够时调度器会自动抢占。--max-num-batched-tokens 8192限制每一轮喂给模型的总 token 数,对 4B 规模的模型来说 8192 已经能撑满算力,再往上提收益递减。两个参数一起调的原则:先确认显存能覆盖并发序列的 KV Cache,再调大 batched tokens 去压算力。

参数默认值/行为作用调大后的代价
max-num-seqs旧版固定上限,V1 按预算动态调整同时并发的序列数KV Cache 压力增大,显存不足时触发抢占
max-num-batched-tokens根据模型自动计算单轮最大 token 计算量单步延迟变长,首 token 延迟可能上升
gpu-memory-utilization默认 0.9给权重之外预留显存比例设太低 KV Cache 不够,设太高权重装不下
max-model-len模型默认序列最大长度越长 KV Cache 占用越大,并发序列越少

提示:max-num-seqs和 KV Cache 块数之间不是精确等价关系,因为每条序列占用的块数会随生成长度变化。线上压测时,以启动日志里实际打印的Allocated blocks为准。

4.2 gpu_memory_utilization 与 KV Cache 预留

这个参数是 vLLM 显存管理的地基。初始化时先估算模型权重占多少显存,再把gpu_memory_utilization指定比例之外的剩余显存,全部切成 KV Cache 物理块。也就是说,你把这个值从 0.85 调到 0.9,多出来的 5% 显存大概率不会给权重,而是变成更多 KV Cache 块,从而提升并发上限。

# 启动前查看当前显存状态,决定 gpu_memory_utilization 取值 import torch free_mem, total_mem = torch.cuda.mem_get_info() print(f"总显存 {total_mem / 1024**3:.1f} GB, 当前空闲 {free_mem / 1024**3:.1f} GB") # 引擎启动日志里会打印实际分配的 cache block 数 # 类似 "Allocated blocks: 4096, block size: 16" 的输出

逻辑说明:torch.cuda.mem_get_info()在启用CUDA_VISIBLE_DEVICES后只显示可见卡的信息。引擎初始化完成打印Allocated blocks后,你可以用“块数 × block size × 每条 token 的 KV 大小 × 层数”估算出 KV Cache 总量,这个数字能直接诊断显存是否用到了实处。坑在于多卡张量并行时,gpu_memory_utilization是按每张卡单独算的,2 卡各 0.9 和 1 卡 0.9 的可用块数完全不同,排障时要按单卡视角去核。

还需要留意 block size 与max_model_len的配合:序列长度超过 block size 的整数倍时,最后一个不满的块照样整块占着,亮是有一条 20 token 的序列在 block size 16 下会占 2 个块,第二个块只有 4 个 token 被用上,剩下 12 个位置是浪费的。所以长上下文场景适当调大 block size 可以减少块数损耗,但块太大又会让短序列浪费更多空间,这个 trade-off 值得在实际压测里权衡。

4.3 调度策略:preemption 模式怎么选

调度器在显存不足时,要么把序列的 KV Cache 搬到 CPU(swap),要么直接丢掉已计算的 KV Cache(recompute),等下次轮到它时重新算。这两个策略对应完全不同的代价模型。0.7.3 里通过--preemption-mode选择,常见值是swap或recompute,也可以同时启用,让调度器自己判断。

模式代价适用场景
swap需要 CPU 内存和 PCIe 带宽,换入换出有延迟显存紧张但 CPU 内存宽裕、对延迟不敏感
recompute没有传输开销,但重算消耗 GPU 算力抢占率高时反而更稳定

我的经验是:如果抢占率超过 5%,先别急着调preemption-mode,先去看是不是max-num-seqs设得太大,或者 KV Cache 预算太低。preemption 只是兜底策略,不是用来调性能的。真到了需要靠换入换出顶着跑的时候,recompute往往比swap在整体吞吐上更稳,因为 swap 省的是算力、花的是时间,这笔账不划算。

5. 避坑:VLLM 0.7.3 源码里最容易翻车的几个点

5.1 现象:OOM 但显存没满

现象:服务运行一段时间后,日志出现CUDA OOM,但nvidia-smi一看显存占用只有 80% 左右,好像还有空余。

原因:vLLM 在启动时就把显存里能用的部分几乎都预留给 KV Cache 了,监控工具看到的“空闲”其实是 cache block 预占的。真正触发 OOM 的是模型前向时把全部 batch 的中间激活值一次性压进显存,那些临时激活值的空间在你预算之外。

解决:第一步降低max-num-batched-tokens或max-num-seqs,让每轮前向的激活值变小;第二步把gpu_memory_utilization调低一点,留出 3% 到 5% 的余量给临时激活。如果启用了--enforce-eager,激活值还会进一步变大,也要纳入预算考虑。

# 排查 CUDA OOM 时的两个快速动作 git log --oneline -5 # 确认本地代码和上游版本是否对齐 python -c "import vllm; print(vllm.__version__)" # 确认版本是 0.7.3

5.2 现象:换模型后吞吐骤降

现象:同一个服务配置,从 7B 模型换成 70B 模型后,吞吐并不按 1/10 的比例下降,而是降到原来的几十分之一,甚至大量排队超时。

原因:70B 模型每条序列的 KV Cache 占用比 7B 大得多。在max-model-len不变的前提下,能并发的序列数急剧减少。如果max-num-seqs还维持原来的值,调度器会产生大量抢占,抢占本身要重算或换入换出,吞吐自然崩。

解决:换模型时同步检查三件事:max-model-len是否还匹配模型能力;max-num-seqs是否需要调低;gpu_memory_utilization是否仍能支撑。比较稳的做法是先用小批量压一遍,观察日志里有没有“preemption”相关计数,如果持续增加,先降并发再谈吞吐。

5.3 现象:流式输出首 token 延迟异常

现象:请求发出后,等了很久第一个 token 才出来,但后续生成速度正常。

原因:首 token 延迟高往往不是模型慢,而是排队慢。V1 调度器在高并发下,新请求在 waiting 队列里可能要等好几轮迭代才能进入 running 队列,这期间没有做任何计算。这种排队延迟被算进首 token 延迟统计里,看起来就很难看。

解决:先确认压测方式是按 token 并发还是按请求数并发。少数长流式请求会占住大量 token 预算,导致后面短请求进不来。可以把max-num-seqs调高一点,或限制单条序列最大长度,避免长序列把预算吃光。真正的病根通常不是采样代码,而是调度器预算分配。

5.4 现象:自定义模型注册不生效

现象:自己写了一个模型文件,但启动时报模型不支持。

原因:vLLM 的模型注册不是扫描所有文件,它靠代码里的模型注册表。只写了模型实现、没把模型名和实现类挂到注册表,引擎就根本不知道这个模型存在。0.7.3 对自定义模型入口有比较明确的约定,但很多人都会漏掉“注册”这一步。

解决:在自定义模型文件里加上注册逻辑,把模型实现类和对应架构名绑定。

# 自定义模型注册示意,架构名按模型 config 里的 architectures 填 from vllm.model_executor.models import ModelRegistry ModelRegistry.register_model( "MyCustomForCausalLM", # 对应 config.json 里的 architectures __import__("my_custom_model").MyCustomModel, # 你的模型实现类 )

写完后重启服务,注意启动日志里有没有你的模型架构名出现。如果没有,说明注册表没加载到那个模块,常见原因是 import 路径写错,或模型实现文件没有被任何入口模块引用。

5.5 现象:版本升级后行为不一致

现象:从 0.6.x 升到 0.7.3 后,同样请求返回的 token 不一样,甚至配置参数直接报错。

原因:0.7.x 引入 V1 执行路径,调度器、采样器、默认 KV Cache 块大小都有变化。老的启动参数有一部分被改名或改变默认值,表现上就是同一个模型、不同输出。

解决:第一看新版启动日志里打印出来的最终生效配置,不要信命令行传的参数;第二对比 0.7.3 里采样参数的默认值,特别是temperature这类。业务对输出稳定性有要求时,把采样参数显式写全,不要依赖默认值。

# 版本迁移时,先保留一份启动参数做对比 vllm serve /data/models/MyLocal-4B-Instruct \ --temperature 0.7 \ --top-p 0.8 \ --max-tokens 2048 \ --max-model-len 8192 \ > server.log 2>&1 & # 然后检查启动日志里的警告和弃用提示 grep -i "warning\|deprecated" server.log

6. 给想要二次开发的人:一个最小可用的自定义采样钩子

很多团队改推理服务的需求,其实落在“某些输入不能输出某些词”这类规则上,这种场景不需要改框架核心,vLLM 的SamplingParams提供了logits_processors参数,能在采样前对 logits 做自定义修改。下面给你一个最小例子:强制禁止某个 token 被输出。

from vllm import LLM, SamplingParams def block_token(token_id): def processor(logits, request_id): logits[:, token_id] = -float("inf") # 将目标 token 的概率置零 return logits return processor llm = LLM(model="/data/models/MyLocal-4B-Instruct") params = SamplingParams( temperature=0.7, max_tokens=256, logits_processors=[block_token(token_id=1234)], ) outs = llm.generate(["需要过滤特定 token 的输入"], params) print(outs[0].outputs[0].text)

逻辑说明:logits_processors里的函数会在温度缩放之前调用,你对 logits 做的修改会被后续 temperature、top_p 步骤影响。函数签名是(logits, request_id),logits形状是[batch, vocab_size],上线前要考虑 batch 维度,不能只写单个样本的逻辑。这个钩子执行位置在 CPU 与 GPU 之间,processor 写得很重会拖慢整个采样阶段,生产环境建议把规则逻辑也挪到 GPU 算子,而不是纯 Python 循环。

我的经验是:不建议一上来就改调度器或 KV Cache 分配逻辑,那些是 vLLM 性能最强也最脆的部分。先从logits_processors这类官方留好的浅钩子入手,验证业务逻辑对不对,再往深水区走。读 0.7.3 源码我踩得最深的一个坑,就是一开始去啃 V1 调度器的预算分配细节,结果大半天没理清,后来转去先读采样路径,再回头看调度器,理解反而快了很多。希望你也能找到一个适合自己的切入点,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 14:39:39

校园考勤太累?轻量化工具实测:扫码+定位,防代签还省时

每到了该点名的课间&#xff0c;我总会在讲台上犹豫一秒。那一秒里要想的是&#xff1a;喊答到吧&#xff0c;得花七八分钟&#xff0c;声音大一点还吵到隔壁教室&#xff1b;发纸签吧&#xff0c;传一圈回来&#xff0c;缺课的几个名字照样工工整整出现在纸上。这不是段子&…

作者头像 李华
网站建设 2026/10/14 14:39:37

神经网络与遗传算法融合的机器人路径规划方法

简介&#xff1a;本资源是一套面向机器人算法学习者与智能系统开发者的路径规划实践项目&#xff0c;聚焦神经网络与遗传算法的协同优化机制&#xff0c;解决动态环境中机器人高效避障与全局最优路径生成的核心问题。压缩包共182个文件&#xff0c;含50个JSON配置与数据文件、4…

作者头像 李华
网站建设 2026/10/14 14:39:34

Zigbee无线传感器网络管理:星型网络组建、设备管理与故障排查

这篇是 Day3 的整理。前两篇我们拆过传感器节点本身&#xff0c;也聊过两个设备之间的点对点通信&#xff0c;今天终于进入真正的“组网”环节&#xff1a;Zigbee 无线传感器网络管理。管理这个词听起来很抽象&#xff0c;落到星型网络上其实就是几件事&#xff1a;谁来当中心节…

作者头像 李华
网站建设 2026/10/14 14:38:51

MySQL SELECT与SHOW实战:从基础查询到性能排查的完整指南

从接触数据库开始&#xff0c;大多数人的第一条SQL都是SELECT&#xff0c;而SHOW往往是那个“我知道它存在&#xff0c;但很少深入研究”的兄弟命令。这个主题虽然叫“简单检索”&#xff0c;但SELECT和SHOW几乎撑起了日常数据库操作的半边天&#xff1a;前者负责从一张或多张表…

作者头像 李华
网站建设 2026/10/14 14:38:41

Vue+Element UI树形控件整合下拉菜单与行内编辑的完整实践

在公司后台管理系统里摸爬滚打久了&#xff0c;你会发现一个特别高频的组合需求&#xff1a;左侧是部门树、栏目树或者权限树&#xff0c;右侧要么是详情&#xff0c;要么是列表&#xff0c;这时候树节点上那"多一个按钮都能踩到误触陷阱"的操作区&#xff0c;就成了…

作者头像 李华
网站建设 2026/10/14 14:38:06

工业故障诊断中SMOTE失效原因与特征空间优化策略

1. 工业故障诊断里那个绕不开的坑搞工业故障诊断的同行&#xff0c;十有八九都碰过这样的场景&#xff1a;设备正常运行的样本一抓一大把&#xff0c;可真正出故障的数据少得可怜&#xff0c;有时候跑一个月才攒下几条异常记录。这种极端不平衡的数据分布&#xff0c;直接导致分…

作者头像 李华