执行摘要
- 一句话:替换pooling子系统中的assert为显式异常
- 推荐动作:建议合并,这是一次低风险的质量改进,展示了如何用显式异常替代assert以提升代码健壮性和用户体验。值得对其他模块类似的assert进行类似替换。
功能与动机
Follow-up to #43286. Replaced the remaining ~10 assert statements across the pooling subsystem with proper exceptions to improve UX.
实现拆解
- seqwise/heads.py:将EmbeddingPoolerHead.forward和ClassifierPoolerHead.forward中的两个assert替换为ValueError。
- seqwise/methods.py:将CLSPool.forward和MeanPool.forward中的两个assert替换为RuntimeError。
- config/pooler.py:将get_seq_pooling_type和get_tok_pooling_type中的两个assert替换为ValueError。
- tokwise/heads.py、seqwise/poolers.py、tokwise/poolers.py:分别替换其中assert为ValueError。
- 测试文件:更新test_pooler_methods.py中期望的异常类型从AssertionError改为RuntimeError。
关键文件:
vllm/model_executor/layers/pooler/seqwise/heads.py(模块 池化头部;类别 source;类型 data-contract;符号 EmbeddingPoolerHead.forward, ClassifierPoolerHead.forward): 核心头部类,替换了两个assert为ValueError,影响embed和classify流程
vllm/model_executor/layers/pooler/seqwise/methods.py(模块 池化方法;类别 source;类型 data-contract;符号 CLSPool.forward, MeanPool.forward): CLSPool和MeanPool中的assert替换为RuntimeError,影响序列池化方法
vllm/config/pooler.py(模块 池化配置;类别 source;类型 core-logic;符号 get_seq_pooling_type, get_tok_pooling_type): 配置访问方法中的assert替换为ValueError,避免未初始化时静默失败
vllm/model_executor/layers/pooler/tokwise/heads.py(模块 Token池化头;类别 source;类型 data-contract;符号 TokenPoolerHead.forward): TokenPoolerHead.forward中assert替换为ValueError
vllm/model_executor/layers/pooler/seqwise/poolers.py(模块 序列池化器;类别 source;类型 data-contract;符号 pooler_for_classify): 序列池化工厂函数中assert替换为ValueError
vllm/model_executor/layers/pooler/tokwise/poolers.py(模块 Token池化器;类别 source;类型 data-contract;符号 pooler_for_token_classify): Token池化工厂函数中assert替换为ValueError
tests/model_executor/layers/test_pooler_methods.py(模块 池化测试;类别 test;类型 test-coverage): 更新测试期望异常类型从AssertionError改为RuntimeError
关键符号:EmbeddingPoolerHead.forward, ClassifierPoolerHead.forward, CLSPool.forward, MeanPool.forward, get_seq_pooling_type, get_tok_pooling_type, TokenPoolerHead.forward, pooler_for_classify, pooler_for_token_classify
关键源码片段
vllm/model_executor/layers/pooler/seqwise/heads.py
核心头部类,替换了两个assert为ValueError,影响embed和classify流程
class EmbeddingPoolerHead(SequencePoolerHead):
def forward(
self,
pooled_data: SequencePoolingMethodOutput,
pooling_metadata: PoolingMetadata,
) -> SequencePoolerHeadOutput:
pooling_params = pooling_metadata.pooling_params
# 原 assert len(pooled_data) == len(pooling_params) 替换为 ValueError
if len(pooled_data) != len(pooling_params):
raise ValueError(
f"pooled_data length ({len(pooled_data)}) does not match "
f"pooling_params length ({len(pooling_params)})"
)
if isinstance(pooled_data, list):
pooled_data = torch.stack(pooled_data)
if self.head_dtype is not None:
pooled_data = pooled_data.to(self.head_dtype)
if self.projector is not None:
embeddings = self.projector(pooled_data)
else:
embeddings = pooled_data
# Matryoshka 维度截断
dimensions_list = [p.dimensions for p in pooling_params]
if any(d is not None for d in dimensions_list):
# 原 assert len(embeddings) == len(dimensions_list) 替换为 ValueError
if len(embeddings) != len(dimensions_list):
raise ValueError(
f"embeddings length ({len(embeddings)}) does not match "
f"dimensions_list length ({len(dimensions_list)})"
)
if len(set(dimensions_list)) == 1 and not isinstance(embeddings, list):
embeddings = embeddings[..., :dimensions_list[0]]
else:
embeddings = [
vecs if d is None else vecs[..., :d]
for vecs, d in zip(embeddings, dimensions_list)
]
# 归一化
if self.activation is not None:
flags = [p.use_activation for p in pooling_params]
if len(set(flags)) == 1:
if flags[0]:
embeddings = self.activation(embeddings)
else:
embeddings = [
self.activation(vecs) if f else vecs
for vecs, f in zip(embeddings, flags)
]
return embeddings
vllm/model_executor/layers/pooler/seqwise/methods.py
CLSPool和MeanPool中的assert替换为RuntimeError,影响序列池化方法
class CLSPool(SequencePoolingMethod):
def forward(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> SequencePoolingMethodOutput:
pooling_cursor = pooling_metadata.get_pooling_cursor()
# 原 assert not pooling_cursor.is_partial_prefill() 替换为 RuntimeError
if pooling_cursor.is_partial_prefill():
raise RuntimeError("partial prefill is not supported with CLS pooling")
return hidden_states[pooling_cursor.first_token_indices_gpu]
class MeanPool(SequencePoolingMethod):
def forward(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> SequencePoolingMethodOutput:
pooling_cursor = pooling_metadata.get_pooling_cursor()
# 原 assert not pooling_cursor.is_partial_prefill() 替换为 RuntimeError
if pooling_cursor.is_partial_prefill():
raise RuntimeError("partial prefill is not supported with MEAN pooling")
prompt_lens_cpu = pooling_cursor.prompt_lens_cpu
num_seqs = prompt_lens_cpu.numel()
hidden_size = hidden_states.shape[-1]
if num_seqs == 0:
return hidden_states.new_empty((0, hidden_size), dtype=torch.float32)
# 剩余 chunked 计算不变 ...
vllm/config/pooler.py
配置访问方法中的assert替换为ValueError,避免未初始化时静默失败
def get_seq_pooling_type(self) -> SequencePoolingType:
# 原 assert self.seq_pooling_type is not None 替换为 ValueError
if self.seq_pooling_type is None:
raise ValueError(
"seq_pooling_type is not set; it should be resolved by"
" ModelConfig before calling get_seq_pooling_type()"
)
return self.seq_pooling_type
def get_tok_pooling_type(self) -> TokenPoolingType:
# 原 assert self.tok_pooling_type is not None 替换为 ValueError
if self.tok_pooling_type is None:
raise ValueError(
"tok_pooling_type is not set; it should be resolved by"
" ModelConfig before calling get_tok_pooling_type()"
)
return self.tok_pooling_type
评论区精华
review中noooop指出model-executor CI失败可能由本PR引起,作者随后更新测试以捕获RuntimeError,修复CI。无其他争议。
- CI failure caused by this PR and resolution (testing): 测试更新后CI修复
风险与影响
- 风险:低风险。异常类型从AssertionError改为ValueError/RuntimeError,任何显式捕获AssertionError的代码可能需要更新,但在pooling模块外直接捕获的可能性很低。配置未初始化时现在抛出明确的ValueError而非AssertionError,行为更合理。
- 影响:用户将看到更具描述性的错误信息(如'pooled_data length does not match pooling_params length'而非简单的AssertionError)。系统功能无变化,开发过程更健壮。
- 风险标记:异常类型变更可能影响异常捕获, 测试覆盖完整性
关联脉络
- PR #43286 Previous pooling assert replacement PR (referenced in body): 此PR是#43286的后续,完成同一件事
参与讨论