invalid Zip archive: Could not find EOCD
主线位置:推理主线第一步「读 config 和权重」,
.pt文件读得进 PyTorch,读不进 candle,或者读进来少了几个 buffer。
版本:PyTorch 2.15.0(CPU)、candle 0.11.0(CPU,accelerate)
flowchart TB
f[".pt 文件"] --> z{"开头是 PK 吗"}
z -->|"80 02 旧格式"| e1["Could not find EOCD"]
z -->|是| s{"storage 类型"}
s -->|"Bool Int"| sk["stderr skipping"]
sk --> e2["cannot find tensor"]
s -->|"Float Long Byte 等"| ok["读到"]
实验代码:PyTorch 侧 make_odd.py,candle 侧 crate pitb-exp 的 bin p16_odd(release)。平台 M2 Max、macOS 27、rustc 1.99.0。candle 认哪几种 storage、怎么处理非连续视图,见 PyTorch 的 .bin / .pt。
PyTorch 的写法
一个小模块,除了 Linear 还用 register_buffer 存了一个 bool 的因果 mask 和 int32 的位置下标,这是 PyTorch 里放「不训练但要随模型保存」的东西的标准写法。存两份:默认格式,和老代码里常见的 _use_new_zipfile_serialization=False:
class Block(nn.Module):
def __init__(self):
super().__init__()
self.proj = nn.Linear(4, 4)
self.register_buffer("causal", torch.ones(4, 4, dtype=torch.bool).tril())
self.register_buffer("pos_ids", torch.arange(4, dtype=torch.int32))
def forward(self, x):
return self.proj(x).masked_fill(~self.causal, 0.0)
torch.manual_seed(0)
m = Block()
sd = m.state_dict()
torch.save(sd, f"{out}/block.pt")
torch.save(sd, f"{out}/block_legacy.pt", _use_new_zipfile_serialization=False)
causal torch.bool [4, 4]
pos_ids torch.int32 [4]
proj.weight torch.float32 [4, 4]
proj.bias torch.float32 [4]
block.pt torch.load ok: ['causal', 'pos_ids', 'proj.bias', 'proj.weight']
block_legacy.pt torch.load ok: ['causal', 'pos_ids', 'proj.bias', 'proj.weight']
legacy head: 80 02 8a 0a 6c fc 9c 46
zip head: 50 4b 03 04
forward: [-0.292263, 0.627516, -0.489109, -1.008023]
torch.load 对两种格式、四个键都没有意见。两个文件的开头不同:默认格式是 ZIP(50 4b 03 04,即 PK\x03\x04),旧格式是 pickle 协议头 80 02 加 PyTorch 的魔数。forward 一行是第 4 行输入的输出,下面拿来对照。
搬到 candle 的现象
candle 侧照着模块写:proj 用 linear,两个 buffer 用 get_with_hints_dtype 按名字取:
fn forward(vb: &VarBuilder) -> Result<Vec<f32>> {
let proj = linear(4, 4, vb.pp("proj"))?;
let causal = vb.get_with_hints_dtype((4, 4), "causal", Default::default(), DType::U8)?;
let _pos = vb.get_with_hints_dtype(4, "pos_ids", Default::default(), DType::I64)?;
let x = (Tensor::arange(0f32, 16.0, vb.device())?.reshape((4, 4))? / 16.0)?;
let y = proj.forward(&x)?;
let y = causal.where_cond(&y, &y.zeros_like()?)?;
Ok(y.get(3)?.to_vec1()?)
}
let r = VarBuilder::from_pth(&path, DType::F32, &dev).and_then(|vb| Ok(forward(&vb)));
stderr 和 stdout 按时间顺序:
--- block_legacy.pt
block_legacy.pt Err: invalid Zip archive: Could not find EOCD
--- block.pt
skipping: unsupported storage type IntStorage
skipping: unsupported storage type BoolStorage
block.pt Err: cannot find tensor causal
- 旧格式:
from_pth本身就失败。报错说的是 ZIP,没有提到 PyTorch,也没有提到「格式太老」。 - 默认格式:
from_pth成功,stderr 多两行skipping:,然后模型要causal时cannot find tensor causal。stderr 的这两行在服务的日志里很容易被淹没,报错本身又只给出名字,看上去和「键名写错」一模一样。
原因
两处都在 candle-core/src/pickle.rs。
read_pth_tensor_info 第一步是 zip::ZipArchive::new。ZIP 的目录在文件末尾,读取器从后往前找「中央目录结尾记录」(End Of Central Directory,EOCD),旧格式的文件是一整段 pickle,没有这条记录。candle 只实现了 PyTorch 1.6 起默认的 ZIP 格式。
ZIP 读通之后,每个张量的 storage 类型由 rebuild_args 按类名换成 DType:
let (_module_name, class_name) = storage.remove(1).class()?;
let dtype = match class_name.as_str() {
"FloatStorage" => DType::F32,
"DoubleStorage" => DType::F64,
"HalfStorage" => DType::F16,
"BFloat16Storage" => DType::BF16,
"ByteStorage" => DType::U8,
"LongStorage" => DType::I64,
other => {
crate::bail!("unsupported storage type {other}")
}
};
torch.bool 是 BoolStorage,torch.int32 是 IntStorage,都落到 bail!。这个 Err 在 read_pth_tensor_info 里被 eprintln!("skipping: {err:?}") 吃掉(同一个函数的循环,见 from_pth 读到 0 个张量),张量表里没有这一项,VarBuilder 只能说找不到。candle 自己有 U32 和 I32(创建与 dtype),只是 pickle 读取这一层没有接上。origin/main 上这张表没有变。
改法
在 Python 里把 bool 和 int32 换成 candle 认得的 dtype 再存,candle 侧用 get_with_hints_dtype 按需要的 dtype 取:
fixed = {k: (v.to(torch.uint8) if v.dtype == torch.bool else v.to(torch.int64) if v.dtype == torch.int32 else v)
for k, v in sd.items()}
torch.save(fixed, f"{out}/block_fixed.pt")
torch.save(torch.load(f"{out}/block_legacy.pt"), f"{out}/block_resaved.pt")
--- block_fixed.pt
block_fixed.pt forward: [-0.29226345, 0.6275161, -0.48910892, -1.0080225]
--- block_resaved.pt
skipping: unsupported storage type IntStorage
skipping: unsupported storage type BoolStorage
block_resaved.pt Err: cannot find tensor causal
block_fixed.pt:输出和 PyTorch 的[-0.292263, 0.627516, -0.489109, -1.008023]在打印的 6 位小数内相同。U8 的 mask 正好是where_cond要的条件类型。block_resaved.pt:只是把旧格式读出来、用默认格式重存。EOCD 的问题没了,dtype 的问题还在。两件事要一起做。
mask 这类能从形状算出来的 buffer,更省事的办法是不读,在 candle 里现建(Tensor::tril2),llama.rs 的因果 mask 就是这样做的(对齐 logits)。
怎么检查
- 文件开头 4 个字节:
head -c 4 f.pt | xxd,504b 0304是 ZIP,8002开头是旧格式。 - Python 里列出 dtype:
{v.dtype for v in torch.load(f).values()},出现torch.bool、torch.int32、torch.int16、torch.uint16之类不在上面表里的,存之前转掉。 - 加载时盯 stderr 里的
skipping:,或者比较read_pth_tensor_info(..).len()和 Python 里的len(state_dict)。
小结
_use_new_zipfile_serialization=False存的旧格式,candle 报invalid Zip archive: Could not find EOCD;candle 只读 ZIP 格式。- 默认格式里
torch.bool、torch.int32的 storage 不在 candle 的六种之内,stderr 打一行skipping: unsupported storage type ...,之后cannot find tensor <名字>。 - 改法:Python 里 bool 转 uint8、int32 转 int64,用默认格式存;只是用
torch.load+torch.save重存,dtype 的问题还在。