Skip to content

SafetensorError: MetadataIncompleteBuffer on an intact checkpoint — safetensors strictness depends on whether DynamicVRAM is active, and the "corrupt/incomplete" message misdirects #15599

Description

@DDavisWA

Summary

A checkpoint that loads fine on one ComfyUI install fails on another with:

safetensors_rust.SafetensorError: Error while deserializing header: MetadataIncompleteBuffer

The safetensors file is corrupt/incomplete. Check the file size and make sure you have
copied/downloaded it correctly.

The file is not corrupt, not incomplete, and not a bad download. It has a few bytes
after the last tensor, and whether that is fatal depends on whether DynamicVRAM is
active.

comfy.utils.load_torch_file picks one of two safetensors parsers based on
comfy.memory_management.aimdo_enabled:

# comfy/utils.py:126-141
if ckpt.lower().endswith(".safetensors") or ckpt.lower().endswith(".sft"):
    try:
        if comfy.memory_management.aimdo_enabled:
            sd, metadata = load_safetensors(ckpt)          # ComfyUI's own parser
        else:
            with safetensors.safe_open(ckpt, framework="pt", device=device.type) as f:
                ...                                        # the strict Rust parser

The two disagree about files that have bytes after the last tensor. ComfyUI's parser
reads the header and walks data_offsets, so it doesn't care. The Rust one rejects the
file as not fully covered by tensor data.

The result is that whether a given checkpoint loads at all depends on a setting that has
nothing to do with parsing.

Reproduction

MiniMax_H3_Ref2VA_pruned_nvfp4.safetensors from
Abiray/Minimax-H3-nvfp4-INT4-INT8-Convrot
has 66 bytes after its tensor data, exactly as HuggingFace serves it — no truncation, no
bad download:

tensors:        1132
data_start:     116,008
declared_end:   12,528,636,800
file_size:      12,528,636,866
trailing_bytes: 66

Those bytes are a marker string left by the conversion tool:

b'\nL2P_bypass_MiniMax_H3_Ref2VA_pruned_nvfp4.safetensors_1785751127\n'

With DynamicVRAM active it loads. Without, it raises
SafetensorError: Error while deserializing header: MetadataIncompleteBuffer.

Who this hits

enables_dynamic_vram() (comfy/cli_args.py:312) returns False for any of:

  • --disable-dynamic-vram
  • --highvram
  • --gpu-only
  • --novram
  • --cpu

and main.py:251 additionally requires NVIDIA and PyTorch >= 2.8.

So the file fails for AMD, Intel and Mac users, anyone on PyTorch < 2.8, and anyone
running --highvram or --gpu-only. That last one inverts how people expect things to
break: a 24 GB card started with --highvram refuses a checkpoint that a 16 GB card on
defaults loads without complaint.

(Checked against master at a779de4.)

The error message sends people the wrong way

comfy/utils.py:147-148 catches this exact exception and says:

The safetensors file is corrupt/incomplete. Check the file size and make sure you have
copied/downloaded it correctly.

For this failure mode that advice is actively misleading. The file is neither corrupt nor
incomplete — it has extra bytes, not missing ones — and its size is exactly what the
server reports. Following the advice means re-downloading 12 GB and failing identically,
possibly more than once.

Suggested fix

I initially assumed the fix was "just use load_safetensors in both branches", but that
looks wrong on a closer read, so taking it in order of how safe it seems:

1. Fix the message (safe, small, biggest payoff per line). You already parse the
header yourself in load_safetensors (comfy/utils.py:94-95), so the check is cheap:
if the declared end of tensor data is <= the file size, the file is not truncated, and
the message can say what is actually true — there are N bytes after the tensor data and
this parser is strict about them. That alone turns an afternoon into a minute, and it is
correct whatever you decide about the parsers.

2. Fall back on the specific exception. On MetadataIncompleteBuffer, if the header
is readable and the declared range fits the file, retry leniently.

3. Use one parser everywhere. Cleanest in principle, but I don't think it's free, and
this may be why the split exists in the first place: load_safetensors imports
comfy_aimdo.model_mmap and returns tensors built with torch.frombuffer over a
read-only mapping, carrying _comfy_tensor_file_slice and _comfy_tensor_mmap_refs on
the storage. Those are exactly what the dynamic path wants and probably not what the
legacy path wants — read-only storage in particular seems likely to matter to in-place
weight patching. So I'd expect (3) to need more thought than (1) or (2), and I may be
missing the reason entirely.

If you'd rather not couple the strict path to aimdo at all, a lenient reader is about
fifty lines of stdlib and torch — header, data_offsets, frombuffer, reshape — with no
aimdo dependency. I wrote one for this and checked it tensor-for-tensor against
safetensors.load_file on a different checkpoint that the strict reader does accept — all
917 tensors matched exactly, dtypes and shapes included.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions