
1. 从一次显卡适配的折腾说起前段时间我在一台混装显卡的机器上部署推理服务机器上同时有一块 Intel 核显和一块 NVIDIA 独显本来想着把模型跑在独显上就完事了结果启动的时候各种报错日志里反复出现设备识别、后端选择、算子回退之类的提示。折腾了大半天才把服务拉起来过程中我翻了不少 vLLM 的源码和 issue才慢慢理解了一个挺有意思的设计问题为什么 vLLM 在适配新 GPU 的时候一边把旧的抽象层拆掉一边又要重新造一套可移植层出来。这个问题乍一看很矛盾。拆抽象通常意味着这东西太厚了、太慢了、太不灵活了那按理说应该往更贴近硬件的方向走怎么反而又加了一层但如果你真的读过 vLLM 近几个版本的代码演进尤其是 EngineCore、Scheduler、Executor 这条链路以及 Flat Model、torch.compile 这些新东西的引入就会发现这套操作背后有一套很清晰的逻辑。它拆的不是抽象本身而是拆掉了那种为了兼容而兼容、把不同硬件硬塞进同一个模具的旧抽象它新建的可移植层解决的是另一个维度的问题——让上层调度和模型逻辑不再被具体硬件绑死同时又不牺牲新硬件的能力释放。这篇文章我想把这件事讲透。适合谁看如果你正在做推理框架的二次开发、要给国产卡或者新架构做适配、或者单纯好奇 vLLM 内部到底怎么组织 EngineCore 和 Scheduler 的交互那这篇应该能给你一些参考。我会尽量用从业者之间聊天的口吻把原理、取舍、实操细节和踩过的坑都摊开讲不堆术语也不搞那种综上所述的总结腔。先说结论方向vLLM 拆旧抽象是因为旧抽象把硬件差异和框架逻辑耦合在了一起导致每接一款新卡都要动核心代码再造可移植层是为了把硬件能力描述和执行策略选择解耦让新 GPU 的接入变成填配置、写后端而不是改调度器。这两件事方向相反目标一致——让框架既能跑得广又能跑得快。2. 旧抽象到底拆掉了什么2.1 旧抽象的历史包袱一切为了统一要理解拆什么得先知道旧抽象是怎么来的。vLLM 早期为了快速支持多种后端采用了一种比较大一统的设计思路把注意力计算、KV Cache 管理、采样这些核心环节抽象成一套相对固定的接口然后针对不同硬件写不同的实现。这套思路在只有 CUDA 一种主流后端的时候没什么问题因为大家硬件特性差不多抽象层带来的开销可以接受。但问题在于当你要接入的硬件越来越多——不同厂商的加速卡、不同架构的 GPU、甚至同一厂商不同代际的产品——这套统一接口就开始变形了。为了兼容某个硬件的特殊能力接口上要加参数为了绕开另一个硬件的限制实现里要加分支。久而久之这个抽象层变成了一个巨大的条件判断集合每加一款硬件核心路径上就多几个 if-else。我在读代码的时候印象特别深某些算子实现里针对不同设备的分支能写好几层读起来非常痛苦。更麻烦的是这些分支往往散落在调度、执行、内存管理各个模块里你想搞清楚这块卡到底走了哪条路径得把整个调用链跟一遍。这就是典型的抽象泄漏——本来想用抽象屏蔽差异结果差异从抽象层的缝隙里全漏出来了。2.2 拆掉的具体对象硬编码的设备分支与耦合的执行路径那么 vLLM 具体拆掉了哪些东西根据我自己的观察和社区讨论主要集中在这么几块。第一块是硬编码的设备判断。旧代码里到处能看到类似如果是某类设备就走这条路径的逻辑这些判断散落在模型层、执行层、内存层。拆掉它们的意思是把这些判断从核心逻辑里挪出去变成可配置、可插拔的策略。第二块是耦合的执行路径。以前 Scheduler 在做调度决策的时候会隐式地假设某种执行模型比如假设所有请求都走同一种批处理方式、假设 KV Cache 的布局是固定的。当新硬件有更高效的批处理方式或者不同的内存布局时这种假设就成了枷锁。拆掉它就是让 Scheduler 只关心要调度什么不关心底层怎么执行。第三块是为兼容而生的中间层。有些抽象层存在的唯一理由就是让 A 硬件和 B 硬件看起来一样但它们既不提升性能也不简化逻辑反而增加了维护成本。这类层是优先被清理的对象。注意拆抽象不等于删代码。很多情况下是把逻辑从核心路径挪到边缘从编译期挪到运行期从硬编码挪到配置。看起来代码量没少但耦合度降下来了。2.3 为什么拆是必须的新 GPU 接入的成本账我算过一笔账。假设每接入一款新 GPU因为旧抽象的存在你需要改动核心调度逻辑、修改内存管理、调整算子实现平均涉及十几个文件、上千行改动还要跑完整的回归测试。这个成本对于框架维护者来说是灾难性的因为硬件迭代速度远快于框架迭代速度。拆掉旧抽象之后接入新硬件的改动被收敛到少数几个后端文件里核心逻辑基本不动。这就是拆的直接收益——把变化点隔离出来。软件工程里有个老原则叫识别变化点并封装它vLLM 这波操作本质上就是在做这件事只不过它面对的变化点是硬件多样性。还有一层考虑是性能。旧抽象为了通用性往往会在热路径上引入额外的函数调用、类型转换、内存拷贝。对于推理这种对延迟极度敏感的场景这些开销累积起来很可观。拆掉它们让热路径更直接是实打实的性能收益。社区里有人反馈某些版本升级后性能波动其实很多时候就是在做这类重构短期可能有回退长期是划算的。3. 可移植层为什么又要重新造3.1 可移植层要解决的真问题能力描述而非接口统一拆完旧抽象问题来了如果什么都不抽象那每款硬件都写一套完整实现维护成本不是更高吗这就是可移植层存在的意义。但关键在于新的可移植层和旧的抽象层解决的是不同的问题。旧抽象试图统一接口——让所有硬件看起来一样调用方不用关心底层。新可移植层试图统一能力描述——它不假装硬件一样而是把每款硬件的能力、限制、偏好明确地描述出来让上层根据这些描述做决策。打个比方。旧抽象像是给所有人发同一套制服不管高矮胖瘦都得穿穿不下的就改制服。新可移植层像是给每个人量体裁衣但裁缝用的尺子和布料标准是统一的。前者追求看起来一样后者追求描述得清楚。这个区别很关键。因为硬件的差异是客观存在的你没法真的让它们一样。强行统一只会导致抽象泄漏。而把差异显式描述出来上层就能针对性地做优化——比如某款卡擅长处理长序列那调度时就优先把长请求分给它某款卡显存带宽高那就多缓存一些 KV。3.2 Flat Model 与 torch.compile 在其中的角色说到可移植层就绕不开 Flat Model 和 torch.compile 这两个东西。它们在这套设计里扮演的角色我理解是这样的。Flat Model 的核心思路是把模型的计算图拍平减少嵌套层次让编译器更容易做全局优化。传统的模型实现往往是层层嵌套的模块调用每层都有自己的逻辑编译器很难跨层优化。拍平之后整个前向计算变成一张相对扁平的计算图优化空间大很多。torch.compile 则是把这套拍平的计算图编译成针对特定硬件优化的代码。它的价值在于你写一份模型逻辑它能针对不同后端生成不同的优化代码。这就天然契合可移植层的需求——上层逻辑统一底层代码因硬件而异中间的翻译工作交给编译器。我实测下来的感受是torch.compile 在推理场景下的收益因模型和硬件而异。有些模型能拿到明显的加速有些则提升有限甚至因为编译开销导致首次推理变慢。所以它不是银弹但在可移植层里作为一个可选的优化通道是合理的——用不用、怎么用交给上层根据硬件能力决定。3.3 可移植层的边界它不做什么理解一个设计有时候看它不做什么比看它做什么更重要。vLLM 这套可移植层我观察下来有几个明确的边界。它不负责调度决策。调度是 Scheduler 的事可移植层只提供硬件能力信息不替 Scheduler 做决定。这样职责清晰Scheduler 的逻辑不会被硬件细节污染。它不强行抹平性能差异。如果某款硬件就是比另一款慢可移植层不会假装它们一样快而是如实描述让上层自己权衡。这避免了为了统一而牺牲性能的老问题。它不绑定具体实现。可移植层定义的是能力和接口的契约具体怎么实现是各后端的事。这意味着新硬件接入时只要满足契约实现方式可以自由发挥。这三条边界恰好对应了旧抽象的三个毛病越权调度、强行统一、绑定实现。新设计是有针对性地在纠偏。4. EngineCore、Scheduler、Executor 的交互怎么变了4.1 三者职责的重新划分要理解可移植层怎么落地得看 EngineCore、Scheduler、Executor 这三者的交互。这是 vLLM 推理链路的核心。EngineCore 是引擎的核心负责管理整个推理生命周期包括请求的接收、状态的维护、结果的返回。Scheduler 负责决定每一步哪些请求参与计算、怎么组批。Executor 负责实际执行计算把调度结果变成硬件上的运算。在旧设计里这三者的边界比较模糊。Scheduler 有时候会直接感知底层执行细节Executor 也会反过来影响调度决策。这种耦合导致换硬件时三边都得动。新设计里职责被重新划清Scheduler 只关心逻辑层面的调度比如优先级、公平性、显存预算Executor 只关心怎么把给定的批次高效执行EngineCore 负责协调并在需要时查询可移植层提供的硬件能力。这样换硬件时主要改 Executor 和可移植层的后端实现Scheduler 基本不动。4.2 调度器如何做到硬件无关Scheduler 要做到硬件无关关键在于它决策所依赖的信息被抽象成了统一的描述。比如显存预算以前可能直接读某个硬件的显存接口现在通过可移植层拿到一个标准化的可用显存数值。再比如批处理大小以前可能硬编码某个上限现在根据硬件能力动态计算。我举个具体的例子。假设有两款卡A 卡显存大但算力一般B 卡算力强但显存小。旧设计下Scheduler 可能得写两套逻辑分别适配。新设计下可移植层告诉 SchedulerA 卡可用显存 X、算力 YB 卡可用显存 X、算力 Y。Scheduler 用同一套算法根据这些数值算出各自的批处理策略。逻辑统一结果因硬件而异。这就是硬件无关的真正含义——不是忽略硬件差异而是把差异变成数据让算法去处理数据而不是让代码去处理差异。4.3 执行器如何承接可移植层的能力Executor 是可移植层能力的直接消费者。它从可移植层拿到硬件能力描述然后决定用哪种执行策略。比如某款硬件支持某种高效的注意力算子Executor 就优先调用不支持就回退到通用实现。这里有个设计上的取舍回退路径要不要保留我的看法是必须保留但要有明确的触发条件和日志。因为硬件能力探测可能出错或者某些边界情况下高效路径不适用没有回退就会直接崩。但回退不能是静默的否则你永远不知道自己在跑慢路径。我在实际部署时就遇到过某款卡理论上支持某个优化算子但因为驱动版本问题实际不可用框架静默回退了性能差了一大截查了好久才发现。后来我养成了习惯启动后先看日志里有没有回退提示有的话优先排查。5. 新 GPU 接入的实操路径5.1 接入前的准备工作如果你要给一款新 GPU 做 vLLM 适配动手之前有几件事得先做。第一搞清楚这款硬件的计算能力画像。包括算力峰值、显存容量和带宽、支持的算子类型、编程模型是类 CUDA 还是别的、驱动和工具链的成熟度。这些信息决定了你能用哪些优化路径。第二确认软件栈的兼容性。PyTorch 能不能跑、torch.compile 支不支持这个后端、有没有现成的算子库。如果 PyTorch 都不支持那适配工作量会大很多得先解决基础问题。第三评估社区现状。这款硬件有没有人已经在做 vLLM 适配有没有相关的 issue 或 PR站在别人的肩膀上能省很多事。我一般会先搜一遍看看有没有踩过同样坑的人。提示接入新硬件前先用一个最小可用的推理脚本验证基础链路能不能跑通别一上来就上完整框架否则出问题很难定位是硬件、驱动还是框架的锅。5.2 可移植层后端的实现要点可移植层后端的实现核心是满足契约。契约定义了哪些能力必须提供、哪些可选、接口长什么样。实现时要注意几点。能力探测要准确且保守。宁可少报能力也不要虚报。虚报会导致上层走了不支持的路径然后崩溃少报只是性能差一点但稳定。我见过为了追求性能虚报能力的实现结果在边界情况下频繁出错得不偿失。接口实现要幂等且无副作用。可移植层的接口会被上层频繁调用如果实现里有状态或者副作用很容易出问题。尽量做成纯查询。错误处理要明确。能力不支持时要返回明确的不支持信号而不是抛异常或者返回一个看起来正常但实际错误的值。上层需要根据这个信号做决策。5.3 从零到跑通的完整流程我把接入流程整理成了一张表方便对照。阶段主要工作关键产出常见坑环境验证驱动、工具链、PyTorch 基础测试能跑通的最小推理脚本驱动版本不匹配能力探测编写可移植层后端的能力查询硬件能力描述虚报能力导致崩溃算子适配实现或对接核心算子可用的注意力、采样实现数值精度不一致执行器对接让 Executor 能调用新后端端到端推理跑通回退路径静默性能调优批处理、内存、编译优化可接受的性能指标过度优化导致不稳定回归测试跑完整测试集稳定性验证边界情况覆盖不足每一步我都建议单独验证别跳步。尤其是能力探测和算子适配这两步出问题最难查。5.4 性能验证与回退策略跑通之后就是性能验证。这里我要强调一个容易被忽略的点建立基线。你得知道这款硬件在最朴素实现下的性能是多少才能判断优化有没有效果。我一般会先用通用实现跑一遍记录延迟和吞吐然后再逐步启用优化路径对比提升。回退策略的设计也很关键。我的做法是分三级一级是算子级回退某个算子不支持就用通用实现二级是路径级回退整条优化路径不可用就退回通用路径三级是全局回退整个后端有问题就禁用。每一级回退都要有日志方便排查。6. 踩过的坑与排查实录6.1 设备识别与后端选择的典型问题设备识别问题是我遇到最多的。混装显卡的机器上框架可能识别错设备或者把请求发到了不该发的卡上。有一次我的服务莫名其妙跑在了核显上性能惨不忍睹查了半天才发现是设备枚举顺序的问题。排查这类问题的思路是先确认框架看到的设备列表对不对再确认它选中的设备是不是你期望的。很多框架支持通过环境变量指定设备实在搞不定就显式指定别依赖自动选择。后端选择的问题类似。有时候框架探测到的后端能力和实际不符导致选了错误的执行路径。这时候要看能力探测的日志对比实际硬件规格找出差异点。6.2 编译与算子回退的隐蔽故障torch.compile 相关的故障比较隐蔽因为它可能在编译期就把问题掩盖了。我遇到过一次某个算子在编译后数值精度出了问题但因为是静默的跑出来的结果只是略微偏差不仔细对比根本发现不了。这类问题的排查方法是关掉编译用 eager 模式跑一遍对比结果。如果 eager 正常、编译异常那就是编译的问题。然后再逐步缩小范围定位到具体是哪个算子、哪个优化 pass 出的问题。算子回退的隐蔽性在于它往往是静默的。我的建议是在开发阶段把回退日志级别调高确保任何回退都能看到。生产环境可以调低但要有监控。6.3 常见问题速查表现象可能原因排查方向解决思路服务启动即崩设备识别失败看设备枚举日志显式指定设备性能远低于预期静默回退到通用路径查回退日志修复优化路径或接受回退结果数值偏差编译优化引入误差eager 对比禁用问题算子编译显存溢出能力探测虚报显存对比实际显存修正能力描述批处理效率低调度未感知硬件特性查调度日志完善能力描述首次推理极慢编译开销看编译耗时预热或关闭编译6.4 几个我自己的避坑心得第一个心得别信文档信日志。文档往往滞后于代码尤其是快速迭代的项目。日志才是真相。我养成了启动后先扫一遍日志的习惯很多问题在日志里都有线索。第二个心得小步验证别一次改太多。接入新硬件时一次只改一个变量改完就验证。这样出问题能快速定位。我见过有人一次性改了一堆配置结果出问题后完全不知道是哪个改动导致的。第三个心得保留一个已知可用的配置作为对照。调优过程中随时能退回这个配置避免越调越乱。这个配置最好记录下来包括所有环境变量和参数。第四个心得关注社区动态。vLLM 迭代很快很多你遇到的问题别人可能已经解决了。定期看看 issue 和 PR能省很多时间。尤其是新硬件适配这种场景社区的力量很重要。7. 这套设计对后续扩展意味着什么7.1 新硬件接入的边际成本在下降从工程角度看这套拆旧抽象、造可移植层的设计最大的价值是让新硬件接入的边际成本下降。以前接一款新卡可能要几周现在如果硬件本身和已有后端相似可能几天就能跑通。这个成本下降对于硬件快速迭代的当下意义很大。我自己的体感是最近几次给新设备做适配工作量确实比一年前小了不少。核心逻辑不用动主要精力花在能力探测和算子适配上。而且因为职责清晰出问题也容易定位。7.2 对国产加速卡适配的参考价值这套设计对国产加速卡适配也有参考价值。国产卡的一个普遍问题是软件栈成熟度参差不齐有的算子库完善有的还在建设中。可移植层的设计允许你有多少能力用多少能力不要求一步到位。先跑通基础路径再逐步启用优化这个渐进式的适配路径很实用。另外能力描述这套机制也方便国产卡厂商自己来写后端。厂商最了解自己的硬件让他们按契约提供能力描述和实现比框架维护者去猜要靠谱得多。7.3 我个人的一些判断最后说点我个人的判断。这套设计方向是对的但落地过程中肯定还有不少细节要磨。比如能力描述的粒度怎么定太粗了没法做精细优化太细了维护成本高。再比如回退策略怎么设计才能既稳定又不掩盖问题这些都是需要持续打磨的。我在实际使用中的体会是这套设计对使用者提出了更高的要求——你得理解硬件能力描述的含义才能用好它。以前那种无脑跑的时代过去了现在想拿到好性能得懂点底层。这未必是坏事但对团队的技术能力是个考验。如果你正在做相关的工作我的建议是先把这套交互链路吃透再动手改。理解设计意图比记住接口重要得多。遇到问题多查日志、多对比、多小步验证基本都能解决。这个领域变化快保持学习的心态比什么都重要。