【Bug已解决】CI fails with dev dependencies: RuntimeError: The size of tensor a (4) must match the size of tensor b (8) at non-singleton dimension 1 解决方案
原始报错:CI fails with dev dependencies: RuntimeError: The size of tensor a (4) must match the size of tensor b (8) at non-singleton dimension 1 场景:CI 流水线安装了一组"开发依赖"(dev dependencies,通常含未锁版本的框架开发版),跑测试时两个张量在维度 1 上形状不一致(4 vs 8),抛
RuntimeError。同样的代码在本地(锁定版本)能过,只在 CI 的 dev 依赖环境下失败。 关键词:依赖版本锁定、CI 可复现、dev vs prod 依赖、张量形状断言、环境一致性。
一、现象长什么样
CI 跑测试:
- CI 安装
dev依赖组(含torch、transformers等的开发/未锁定版本); - 测试里两个张量做运算,期望维度 1 都是 4,但一个实际是 8;
- 抛
RuntimeError: The size of tensor a (4) must match the size of tensor b (8) at non-singleton dimension 1; - 本地用锁定版本跑同测试通过;
- 回退 CI 到生产依赖版本,测试也通过。
根因是CI 的 dev 依赖装了一版与代码假设不同的框架,框架内部某处(如默认 head 维度、token 展开数)从 4 变成了 8,导致张量形状不匹配。本质不是算法错,是环境不一致。
二、背景:为什么 dev 依赖会改张量形状
深度学习框架的某些"默认值/内部展开数"会随版本变化。比如某版把某层的隐藏维默认从 4 改到 8,或某 tokenizer 的图像 token 展开数变化。代码若隐式依赖这些默认值(没显式传参固定),版本一变形状就变。
dev 依赖组通常为了"尝试新特性"装了未锁版本(如torch>=2.x.dev或 git 主干),而 prod/本地用requirements.lock锁了具体版本。于是 CI 和本地环境漂移,张量形状假设被打破。
三、根因:依赖未锁定 + 形状假设隐式
根因拆解:
- dev 依赖未锁版本:
dev-requirements.txt写torch(无版本),装到形状默认不同的开发版。 - 形状假设隐式:代码依赖框架默认值(如 head_dim=4)却没显式传,版本一变就错。
- 环境漂移:CI 用 dev 组,本地用 lock 文件,两边框架版本不同。
- 失败晚:形状不匹配直到张量运算才炸,没在构造处早校验。
- 无复现保障:CI 没用锁定文件,每次可能装到不同版本。
下面用最小模型复现"版本不同导致形状假设被打破",再给修复。
四、最小可运行复现
# 模拟不同框架版本下,某默认维度不同 FRAMEWORK_HEAD_DIM = 8 # CI 的 dev 版默认 8;本地 lock 版是 4 def build_qkv(hidden): # 代码隐式依赖 head_dim=4,但没写死,用了框架默认 head_dim = FRAMEWORK_HEAD_DIM # 假设 hidden 按 4 算出的段数 assert hidden % 4 == 0, "hidden 必须能被 4 整除" return hidden // 4, head_dim def combine(a, b): # a, b 期望在 dim1 同形 if a.shape[1] != b.shape[1]: raise RuntimeError( f"The size of tensor a ({a.shape[1]}) must match the size " f"of tensor b ({b.shape[1]}) at non-singleton dimension 1") if __name__ == "__main__": import torch # 本地 lock 版 head_dim=4 时,a 的 dim1=4;CI dev 版 head_dim=8,b 的 dim1=8 a = torch.randn(2, 4) # 按本地假设构造 b = torch.randn(2, 8) # CI dev 版默认值导致 try: combine(a, b) except RuntimeError as e: print("CI 失败:", e) # 形状 4 vs 8 不匹配运行抛形状不匹配——dev 版默认值让b的 dim1 成了 8,与本地假设 4 冲突。
五、方案:锁定依赖版本,CI 用 lock 文件
第一层:所有环境(含 CI dev 组)都必须有锁定版本,dev 组不应装"无版本约束"的框架:
# requirements.lock —— 所有环境统一用 torch==2.5.1 transformers==4.46.0 # dev 组只能在此基础上加工具链,不能改框架大版本 pytest==8.3.0 ruff==0.6.0# ci_install.sh 思路(示意,非 Python) # pip install -r requirements.lock # pip install -r requirements.dev # dev 只加 lint/test 工具,不改 torch 版本通过 lock 文件,CI 装的框架版本与本地完全一致,形状默认值不变,4 vs 8 不再发生。
六、方案:dev / prod 依赖分离且互不污染框架
第二层:dev 依赖组只放工具链(lint、test、coverage),不放会改变运行时行为的框架未锁版本;框架版本只由主 lock 管:
# 依赖分层示例 MAIN = { # 运行时/训练,必须锁定 "torch": "==2.5.1", "transformers": "==4.46.0", } DEV = { # 仅开发工具,不影响张量形状 "pytest": "==8.3.0", "ruff": "==0.6.0", "coverage": "==7.6.0", } def resolve(purpose): if purpose == "train": return MAIN if purpose == "ci-test": # 框架用 MAIN 的锁定版,DEV 只补工具 return {**MAIN, **DEV} raise ValueError(purpose) if __name__ == "__main__": ci = resolve("ci-test") assert ci["torch"] == "==2.5.1" # CI 也用锁定版 print("CI 依赖:", ci)dev 组与框架版本解耦,CI 不会再因 dev 依赖偷偷升级框架而形状漂移。
七、方案:形状断言,早失败且信息明确
第三层:代码里对"依赖框架默认值的形状"加显式断言,一旦版本导致默认值变化,在构造处就报错而非运算时才炸:
import torch EXPECTED_HEAD_DIM = 4 # 显式写死假设,不靠框架默认 def build_heads(x: torch.Tensor): # 显式校验维度,而不是假设框架默认 assert x.shape[1] % EXPECTED_HEAD_DIM == 0, ( f"输入 dim1={x.shape[1]} 不能被 head_dim={EXPECTED_HEAD_DIM} 整除," f"可能框架版本导致默认值变化") return x.shape[1] // EXPECTED_HEAD_DIM def combine_safe(a: torch.Tensor, b: torch.Tensor): if a.shape[1] != b.shape[1]: raise RuntimeError( f"dim1 不匹配 a={a.shape[1]} b={b.shape[1]}(请检查框架版本是否一致)") return a + b if __name__ == "__main__": a = torch.randn(2, 4) b = torch.randn(2, 4) print("heads:", build_heads(a)) # 构造处就校验 print("combine:", combine_safe(a, b).shape)显式断言把"版本导致形状变"的失败点前移到构造处,且错误信息提示"查框架版本",排错更快。
八、验证:把"环境/形状一致性"锁进测试
def test_locked_framework_in_ci(): ci = resolve("ci-test") assert ci["torch"] == "==2.5.1" # CI 与本地同版本 def test_head_dim_assertion_catches_drift(): x = torch.randn(2, 8) # 模拟 dev 版默认值变 8 try: build_heads(x) assert False except AssertionError: pass # 构造处即发现,不拖延到运算 if __name__ == "__main__": test_locked_framework_in_ci() test_head_dim_assertion_catches_drift() print("依赖锁定与形状断言测试通过。")九、排查清单("CI dev 依赖张量形状错"按顺序查)
- 环境对比:本地能过、CI 失败?先怀疑 CI 装的框架版本不同。
- 依赖锁定:CI 是否用 lock 文件?dev 组是否装了无版本约束的框架?
- 依赖分层:dev 组是否只含工具链,没改运行时框架大版本?
- 形状假设:代码是否隐式依赖框架默认值(head_dim 等)?应显式写死。
- 失败时机:形状错在运算时才炸,还是构造处有断言?早失败更好排。
- 复现:CI 每次装的版本是否固定?不固定则会时好时坏。
- 错误信息:形状报错是否提示"可能版本不一致"?方便定位环境。
十、小结
"CI 因 dev 依赖张量形状 4 vs 8 失败"是依赖未锁定导致 CI 与本地框架版本漂移,打破代码隐式的形状假设。修复三层:
- 锁定版本:所有环境(含 CI dev 组)用同一 lock 文件,框架版本固定;
- 依赖分层:dev 组只放工具链,不污染运行时框架版本;
- 形状断言:对依赖默认值的形状显式校验,版本漂移在构造处即暴露。
核心原则:CI 必须是可复现的,依赖版本要锁定而非"装最新"。任何隐式依赖框架默认值(如 head 维度、展开数)的代码,都要么显式写死、要么加断言——否则版本一变,张量形状就会在 CI 上悄悄崩。