执行摘要
- 一句话:bitsandbytes 支持迁移至 OOT 插件,核心删除约 1900 行
- 推荐动作:值得精读,尤其是 linear.py 中 bnb 分支到通用
shard_indexer hook 的重构思路,以及删除 800+ 行专有 loader 后如何保持测试覆盖。关注点包括:adjust_shard_indexes 的通用性设计、插件注册机制、以及未落实的性能 guard 是否会在后续修复。适合对量化插件化和权重加载架构感兴趣的工程师。
功能与动机
关联 RFC Issue #39583 指出 bitsandbytes 与 GGUF 两个量化后端在 vLLM 中使用率极低(约 0.5% 与 0.1%),却给共享权重加载路径(linear.py、fused_moe/layer.py、vocab_parallel_embedding.py 等)引入了约 95 行分支,且未迁移到 weight_loader_v2,严重阻碍了核心加载基础设施的维护和重构。本 PR 作为该 RFC 的第一阶段,优先将 bitsandbytes 迁移为 OOT 插件,以还原 linear.py 的可读性并 unblock 后续清理。
实现拆解
实现分四步完成:
-
删除内置 bitsandbytes 实现:整体移除 vllm/model_executor/layers/quantization/bitsandbytes.py(614 行,含 BitsAndBytesConfig、BitsAndBytesLinearMethod、is_layer_skipped_bnb 等)和 vllm/model_executor/model_loader/bitsandbytes_loader.py(834 行,含 BitsAndBytesModelLoader 及其预量化/在线量化迭代器),并清理 model_loader/__init__.py、quantization/__init__.py 中的注册与导入。
-
泛化共享路径的 shard 逻辑:在 vllm/model_executor/layers/linear.py 中删除 adjust_bitsandbytes_4bit_shard 及 ColumnParallelLinear、MergedColumnParallelLinear、QKVParallelLinear、RowParallelLinear 等 weight_loader 内所有 use_bitsandbytes_4bit 分支,改为调用通用的 adjust_shard_indexes(param, shard_offsets, shard_id, ...),插件可通过设置 param.shard_indexer 注入自定义 shard 偏移计算。同时删除 fused_moe/routed_experts.py 中的 BNB MoE 特例分支。
-
清理配置与加载契约:从 vllm/config/model.py 移除 _verify_bnb_config(8-bit 强制 eager 的 fallback),从 vllm/engine/arg_utils.py 移除 quantization==bitsandbytes 时强制 load_format 的隐式改写,删除 model_loader/utils.py 中仅被 BNB loader 使用的 ParamMapping 数据结构(迁移至插件侧),并同步调整 vllm/config/load.py、vllm/platforms/rocm.py、openpangu.py 等零星引用。
-
测试与 CI 配套迁移:将原 tests/models/quantization/test_bitsandbytes.py 迁移为 tests/plugins_tests/bitsandbytes/test_bitsandbytes.py,测试内改为 from vllm_bnb_plugin import bitsandbytes_loader as bnb,移除了 is_quant_method_supported 的 skip 条件;新增 tests/plugins_tests/bitsandbytes/test_transformers.py 覆盖 transformers 后端量化一致性;在 .buildkite/test_areas/plugins.yaml 增加 BitsAndBytes Plugin 的 CI 步骤(软失败),并删除 tests/kernels/moe/test_moe_weight_loading_padded.py 中针对 BNB shape mismatch 的测试。文档和 example 同步更新插件安装方式。
关键文件:
vllm/model_executor/layers/quantization/bitsandbytes.py(模块 量化层;类别 source;类型 deletion;符号 _check_bitsandbytes_version, BitsAndBytesConfig, BitsAndBytesLinearMethod, is_layer_skipped_bnb): 内置 BNB 量化配置与 LinearMethod/MoEMethod 的完整实现被整体删除(614 行),是迁移的核心对象。
vllm/model_executor/model_loader/bitsandbytes_loader.py(模块 模型加载;类别 source;类型 deletion;符号 BitsAndBytesModelLoader, _get_weight_files, _prepare_weights, _hf_weight_iter): 独立的 BNB 模型加载器(834 行)被整体移除,其职责由 vllm-bnb-plugin 中的对应 loader 承接。
vllm/model_executor/layers/linear.py(模块 线性层;类别 source;类型 data-contract;符号 adjust_bitsandbytes_4bit_shard, adjust_shard_indexes, MergedColumnParallelLinear.weight_loader, QKVParallelLinear.weight_loader): 共享权重加载路径的核心文件,删除约 113 行 BNB 分支并引入泛化的 adjust_shard_indexes hook,是本次重构收益最大的文件。
vllm/model_executor/model_loader/utils.py(模块 模型加载;类别 source;类型 data-contract;符号 ParamMapping, post_init, get_sub_modules): 删除仅被 BNB loader 使用的 ParamMapping 数据结构,进一步清理加载工具层。
tests/plugins_tests/bitsandbytes/test_bitsandbytes.py(模块 插件测试;类别 test;类型 rename-or-move;符号 log_generated_texts, validate_generated_texts, test_load_pp_4bit_bnb_model): BNB 测试从 tests/models/quantization 迁移到 tests/plugins_tests,并改为依赖 vllm_bnb_plugin,验证插件化方案端到端可用。
vllm/config/model.py(模块 配置层;类别 source;类型 data-contract;符号 _verify_bnb_config): 删除 _verify_bnb_config(8bit 强制 eager 的校验),使配置层不再感知 BNB 内部限制。
关键符号:_get_quantized_weights_iterator, _quantized_4bit_generator, adjust_shard_indexes, adjust_bitsandbytes_4bit_shard, _verify_bnb_config, validate_generated_texts
关键源码片段
vllm/model_executor/layers/quantization/bitsandbytes.py
内置 BNB 量化配置与 LinearMethod/MoEMethod 的完整实现被整体删除(614 行),是迁移的核心对象。
# vllm/model_executor/layers/quantization/bitsandbytes.py ( 已删除 )
# 以下是迁移前 BNB 4bit/8bit 权重创建的核心逻辑,
# 展示了量化权重如何以 packed uint8 形式注册到 layer。
class BitsAndBytesLinearMethod(LinearMethodBase):
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: list[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
from bitsandbytes.nn import Int8Params
def create_qweight_for_8bit():
# 8bit 权重使用 bitsandbytes 的 Int8Params,
# 通过 has_fp16_weights 控制是否保留 fp16 副本
qweight = Int8Params(
data=torch.empty(
sum(output_partition_sizes),
input_size_per_partition,
dtype=torch.int8,
),
has_fp16_weights=self.quant_config.llm_int8_has_fp16_weight,
requires_grad=False,
)
set_weight_attrs(qweight, {
"input_dim": 0,
"output_dim": 0,
"pack_factor": 1,
"use_bitsandbytes_8bit": True,
"generation": 0,
})
return qweight
def create_qweight_for_4bit():
# 4bit 权重按 quant_ratio 打包为 uint8 张量,
# 并打上 use_bitsandbytes_4bit 标记供 weight_loader 识别
quant_ratio = calculate_quant_ratio(params_dtype)
total_size = input_size_per_partition * sum(output_partition_sizes)
if total_size % quant_ratio != 0:
raise ValueError(
"The input size is not aligned with the quantized weight shape."
)
qweight = BitsAndBytesWeightParameter(
torch.empty(total_size // quant_ratio, 1, dtype=torch.uint8),
requires_grad=False,
)
set_weight_attrs(qweight, {
"input_dim": 0,
"output_dim": 0,
"pack_factor": quant_ratio,
"use_bitsandbytes_4bit": True,
})
return qweight
if self.quant_config.load_in_8bit:
qweight = create_qweight_for_8bit()
else:
qweight = create_qweight_for_4bit()
layer.register_parameter("weight", qweight)
set_weight_attrs(qweight, extra_weight_attrs)
vllm/model_executor/model_loader/bitsandbytes_loader.py
独立的 BNB 模型加载器(834 行)被整体移除,其职责由 vllm-bnb-plugin 中的对应 loader 承接。
# vllm/model_executor/model_loader/bitsandbytes_loader.py ( 已删除 )
# 以下是预量化 4bit 权重的加载流程核心:
# 第一遍收集 quant_state 到 CPU,第二遍按需组装 QuantState。
def _quantized_4bit_generator(self, hf_weights_files, use_safetensors, quant_state_dict):
from bitsandbytes.functional import QuantState
# 先遍历一遍所有权重,把 quant_state 相关的张量收集到临时字典
weight_iterator = self._hf_weight_iter(hf_weights_files, use_safetensors)
temp_state_dict = {}
for org_weight_name, mapped_weight_name, weight_tensor in weight_iterator:
if not self._is_4bit_weight_name(mapped_weight_name):
continue
if "quant_state.bitsandbytes" in mapped_weight_name:
# bitsandbytes 库要求 quant_state 在 CPU 上
temp_state_dict[mapped_weight_name] = weight_tensor.cpu().data
else:
temp_state_dict[mapped_weight_name] = weight_tensor
def _parse_quant_state(param_name: str, temp_state_dict: dict) -> QuantState:
quant_state = {}
for k in temp_state_dict:
if param_name + "." in k:
quant_state[k] = temp_state_dict[k]
return QuantState.from_dict(quant_state, device=current_platform.device_type)
# 第二遍只产出真正的权重张量,并把 quant_state 挂到权重上
for org_weight_name, mapped_weight_name, weight_tensor in self._hf_weight_iter(
hf_weights_files, use_safetensors
):
if self._is_4bit_weight_name(mapped_weight_name):
continue
if mapped_weight_name in temp_state_dict:
set_weight_attrs(weight_tensor, {"quant_state": _parse_quant_state(mapped_weight_name, temp_state_dict)})
yield org_weight_name, weight_tensor
vllm/model_executor/layers/linear.py
共享权重加载路径的核心文件,删除约 113 行 BNB 分支并引入泛化的 adjust_shard_indexes hook,是本次重构收益最大的文件。
# vllm/model_executor/layers/linear.py ( 迁移后 )
# MergedColumnParallelLinear 的 shard 循环:
# 不再直接判断 bnb,而是通过 param.shard_indexer 委托给量化插件。
def weight_loader(self, param, loaded_weight, loaded_shard_id=None):
self.validate_shard_id(loaded_shard_id)
param_data = param.data
output_dim = getattr(param, "output_dim", None)
if loaded_shard_id is None or isinstance(loaded_shard_id, tuple):
# 权重已在磁盘上融合(例如 mlp 的 gate_up_proj)
if output_dim is None:
param_data.copy_(loaded_weight)
return
output_sizes = self.output_sizes
current_shard_offset = 0
shard_offsets: list[tuple[int, int, int]] = []
for i, output_size in enumerate(output_sizes):
shard_offsets.append((i, current_shard_offset, output_size))
current_shard_offset += output_size
packed_dim = getattr(param, "packed_dim", None)
for shard_id, shard_offset, shard_size in shard_offsets:
if packed_dim == output_dim:
# 按 pack_factor 换算 packed 空间的切分偏移
shard_size //= param.packed_factor
shard_offset //= param.packed_factor
# 通用量化插件 hook:BNB 等后端通过 shard_indexer
# 将偏移从逻辑空间映射到量化 packed 空间
shard_indexer = getattr(param, "shard_indexer", None)
if shard_indexer is not None:
index = list(itertools.accumulate([0] + self.output_sizes))
orig_offsets = {str(i): (index[i], size) for i, size in enumerate(self.output_sizes)}
orig_offsets["total"] = (self.output_size, 0)
shard_size, shard_offset = adjust_shard_indexes(
param, orig_offsets, str(shard_id), shard_size, shard_offset
)
loaded_weight_shard = loaded_weight.narrow(output_dim, shard_offset, shard_size)
self.weight_loader(param, loaded_weight_shard, shard_id)
return
assert loaded_shard_id < len(self.output_sizes)
# 单 shard 路径的逻辑与上面类似,省略细节
...
评论区精华
核心讨论集中在三处:
-
性能回退风险(gemini-code-assist):在 linear.py 的 MergedColumnParallelLinear.weight_loader 和 QKVParallelLinear.weight_loader 中,orig_offsets / orig_qkv_offsets 字典在 shard 循环内对每个参数无条件构造,即使未使用自定义量化插件也会产生额外开销。建议用 getattr(param, "shard_indexer", None) is not None 保护。从合并后的代码看,adjust_shard_indexes 被无条件调用,该性能顾虑并未在本次 PR 中完全落实。
-
ParamMapping 死代码(hmellor):建议删除 model_loader/utils.py 中已无引用的 ParamMapping。Isotr0py 回应称插件侧仍在使用,约定在后续 PR 中迁移至插件仓库。
-
ROCm CI 覆盖(AndreasKaratzas):要求将新增的 BitsAndBytes Plugin 测试组同步到 test-amd.yaml,确保 ROCm 上 bitsandbytes 0.49.2 弃用后功能正常。Isotr0py 表示已在计划中,将放入后续 PR 处理。
-
linear.py 中 shard offsets 计算缺乏 guard 导致性能开销 (performance): 合并后代码中 adjust_shard_indexes 仍被无条件调用,性能 guard 未落实;可能遗留为后续优化点,存在加载性能轻微回退的风险。
- ParamMapping 是否为死代码 (refactor): 确认 ParamMapping 在当前 PR 中保留,待后续 PR 移至插件仓库;当前从 model_loader/utils.py 中删除并保留在内存模块供插件导入(实际迁移到插件侧)。
- ROCm CI 是否覆盖 BitsAndBytes Plugin 测试 (testing): Isotr0py 表示将在后续 PR 中处理,本次只加入软失败 CI 步骤,ROCm 覆盖暂缺。
风险与影响
-
破坏性迁移:移除内置 bitsandbytes 支持后,未安装 vllm-bnb-plugin 的用户直接使用 --quantization bitsandbytes 会报未知量化方法错误。虽然文档更新了安装指引,但迁移窗口期可能影响存量用户。
-
性能回归隐患:linear.py 的 weight_loader 是模型加载热路径,gemini 指出的无条件 itertools.accumulate 与 dict 构造在每参数循环中执行,对非量化模型引入不必要的计算开销;尤其在 QKVParallelLinear 中构造 5 个 key 的偏移字典,可能拖慢大模型加载时间。
-
契约外露风险:adjust_shard_indexes 和 shard_indexer 成为新的公共 hook,如果插件侧实现与核心预期不一致(如 shard_offsets 结构变化),可能导致加载错误。此外 ParamMapping 仍被插件引用,核心删除后插件需要自行维护,存在版本兼容风险。
- 影响:影响范围较大:核心权重加载路径(linear.py、fused_moe、model_loader)净减约 1500+ 行,显著提升可读性和可重构性;对使用 bitsandbytes 的用户是破坏性变更,需额外安装插件包;对 CI 增加软失败插件测试组(暂未覆盖 ROCm);对 GGUF 的后续迁移提供了可复用的 OOT 插件模式。团队内部需要同步维护 vllm-bnb-plugin 仓库,并在后续版本中跟进 ParamMapping 迁移和 ROCm CI。
- 风险标记:核心路径变更, 破坏性迁移, 性能回退风险, 后续跟进项
关联脉络
参与讨论