ComfyUI “Expected All Tensors to Be on the Same Device”: Diagnose and Fix the CPU/GPU Mismatch

Few ComfyUI errors are as frustrating as the one that interrupts a KSampler mid-run with a single terse line:

RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cpu and cuda:0! (when checking argument for argument mat1 in method wrapper_CUDA_addmm)

The message looks like a deep framework bug, but it is almost always a configuration mismatch: one part of your graph is running on the CPU while another expects the GPU. This guide walks through the symptom, the most common causes, how to isolate the offending node or flag, the traps that keep people stuck, and how to verify the fix without guessing.

Because device placement in ComfyUI is determined by a mix of launch flags, per-node dtype choices, and custom-node code, the fix is never a single button. It is a short, methodical diagnostic. I will point to the exact command-line arguments as defined in the ComfyUI source so you are reasoning from authoritative behavior rather than folklore.

What the Error Actually Means

The error is raised by PyTorch when an operation receives two tensors that live on different devices. In a diffusion sampler, the most common trigger is the matrix multiplication inside a linear layer: the model weights sit on cuda:0 while a latent, text-embedding, or conditioning tensor has been placed on cpu (or vice versa). PyTorch will not silently move tensors around for you; it fails fast and prints the two devices it found.

The key clue is the last part of the message — when checking argument for argument mat1 in method wrapper_CUDA_addmm. The addmm reveals the failure is inside a fully-connected layer, not in VAE decode or sampling noise. That already narrows your search to anything that feeds a Linear layer: the UNet/diffusion model itself, text encoders (CLIP/T5/umt5), or a LoRA adapter that attaches to those.

When you see cpu and cuda:0 together, ask one question: what is forcing a component onto the CPU while the rest of the graph stays on the GPU? There are only a handful of answers, and they are all addressable.

Cause 1: VRAM Offload Flags Splitting the Graph

ComfyUI has a family of command-line flags that deliberately run parts of the pipeline on the CPU to save VRAM. When these are mismatched — or when a custom node does not cooperate with them — you get exactly this device split.

The flags are defined in the ComfyUI source (comfy/cli_args.py), and their help text is the clearest reference for what each one does:

  • --gpu-only — “Store and run everything (text encoders/CLIP models, etc.) on the GPU.”
  • --lowvram — runs the text encoders on the CPU when dynamic VRAM is not enabled.
  • --novram — “when lowvram isn’t enough,” offloading even more aggressively.
  • --cpu — run everything on the CPU (slow, and a common source of mismatches if you have a GPU too).
  • --cpu-vae — “Run the VAE on the CPU.”
  • --async-offload and --disable-async-offload — control how weights move between RAM and VRAM.

A frequent story: a user adds --lowvram or --cpu-vae to squeeze a large model onto an 8 GB or 16 GB card, then loads a custom node that assumes every input is a CUDA tensor. The built-in nodes handle offloading correctly, but a third-party sampler or a custom KSampler wrapper can blindly call .to(device) on only some of its inputs, producing the split. The fix is not to remove VRAM saving entirely — it is to identify which node is not offload-aware and either replace it or stop offloading the component it depends on.

Start by reading your launch command. If you are using any of the offload flags, reproduce the error with --gpu-only (or simply without the offload flag, if your card can fit the model) and see whether the error disappears. If it does, you have isolated an offload incompatibility rather than a broken workflow.

Cause 2: Mixed-Precision and VAE Dtype Flags

A second, subtler source of device mismatch is precision flags that change where a component executes. The relevant flags are also in cli_args.py:

  • --cpu-vae (again) — forces VAE decode onto the CPU.
  • --fp16-vae and --fp32-vae / --bf16-vae — select VAE precision. The --fp16-vae help text even warns it “might cause black images.”
  • --force-fp16 / --force-fp32 — global precision forcing.
  • --fp8_e4m3fn-text-enc and friends — store text encoder weights in fp8.

Text encoders are the classic offender here. Wan2.2, Flux, and other new pipelines load a large T5/umt5 text encoder that users routinely quantize to fp8 or offload to CPU to fit in VRAM. If the encoder weights are stored with --fp8_e4m3fn-text-enc but a downstream conditioning node expects them in bf16 on the GPU, PyTorch can surface the device error while it is really a dtype-and-placement disagreement.

The diagnostic is the same shape as before: temporarily remove the precision flag, run once, and watch whether the error location moves or clears. If the failure moves from the text encoder into the sampler, you have confirmed the encoder precision was the trigger. (For a closely related, name-specific failure — the umt5 text encoder refusing to load — see our earlier note on fixing the umt5-xxl fp8 filename; the two issues often appear together when a filename and a dtype both go wrong.)

Cause 3: Custom Nodes That Assume a Device

The third cause is the most common on a heavily extended install: a custom node that hardcodes or mis-infers a device. Any node that calls torch.Tensor.to(), torch.zeros_like, or torch.empty with an explicit or implicit CPU device can introduce the split, even when every launch flag is correct.

To tell whether the node or the flag is at fault, use ComfyUI’s built-in node-by-node execution. Open the workflow, disable --async-offload if it is on, and run. Then read the full traceback where the error is raised — the line that fails references the node’s class_def or its def input_types. Note the node type, then search its source in custom_nodes/ for the word .to( or device=. This is a text search, not guesswork, and it almost always pinpoints the offending line.

Common culprits include:

  • Video-pipeline wrappers (WanVideo, AnimateDiff) that build frame tensors with torch.zeros but forget to move them to the model’s device.
  • Upscaler or face-restoration nodes that run a sub-model on CPU by design but forward GPU latents into it.
  • LoRA loaders that patch a model in memory but leave the base weights on a different device.

When you find the offending node, the short-term fix is either to update it, replace it with a maintained equivalent, or stop combining it with the offload/precision flag that the traceback implicates.

A Repeatable Diagnostic Sequence

Rather than toggling settings at random, work through this fixed order. It separates “flag problem” from “node problem” in minutes:

  1. Capture the launch flags. Note every --lowvram, --cpu-vae, --gpu-only, --force-fp16, and --fp8/--bf16 flag you are using, straight from your startup script.
  2. Read the devices in the error. The pair at the end (“cpu and cuda:0”) tells you the two locations; the operation (addmm, add, copy_) tells you where in the pipeline.
  3. Bisect with a clean run. Drop all offload and precision flags (if VRAM permits) and run the same workflow. Error clears → it is a flag/node interaction. Error persists → it is the workflow or a node.
  4. Re-add flags one at a time. When the error returns, the last flag you re-added is the culprit for that node.
  5. Locate the node. Read the full traceback to get the node class, then grep its source for device or precision calls.

This sequence respects one important reality: the correct reference for every flag’s exact meaning is the source help text linked below, not a forum summary. Flags drift between versions, and a flag that meant one thing in 2025 can change by 2026.

Common Traps That Keep You Stuck

  • Reinstalling PyTorch or ComfyUI first. The error is a placement bug, not a broken install. Reinstalling delays the real fix and can change your CUDA version, introducing new problems.
  • Assuming it is always the VAE. The addmm signature points at a linear layer, which is the diffusion model or text encoder — not the VAE decode. Chasing the VAE wastes time when the traceback says otherwise.
  • Stacking every VRAM flag at once. --lowvram + --cpu-vae + --novram together create multiple CPU/GPU boundaries that multiply the places a mismatch can appear. Use the minimum offload that fits your card.
  • Ignoring the traceback in favor of the forum fix. The exact argument name in the error is your best clue. Read it before you search.

Verifying the Fix

After you make a change, verify it with a deliberate method, not a single lucky render:

  • Run the workflow three times in a row. A device mismatch is deterministic on a given graph — if it clears once it clears consistently, but a flaky appearance means an offload timing issue (--async-offload) rather than a hard placement bug.
  • Watch startup and run logs for device-placement lines. ComfyUI logs which component is being loaded where; confirm the text encoder and VAE are on the device you intend.
  • If you changed a flag, record the working command so the fix survives a restart and a tool upgrade.

For the broader picture on how ComfyUI’s VRAM flags interact — --lowvram, --normalvram, --cpu, and the smarter dynamic offloading — see our earlier deep-dive on ComfyUI VRAM launch flags, and for the CUDA-out-of-memory side of the same coin on a 16 GB card, our note on llama.cpp CUDA OOM applies the same offload reasoning to local LLM inference.

Conclusion

“Expected all tensors to be on the same device” is a routing problem, not a mystery. It happens because ComfyUI legitimately splits work across CPU and GPU to fit large models — and a mismatch between a launch flag, a precision setting, or a custom node’s assumptions breaks that split. Work systematically: capture your flags, read the devices and operation in the traceback, bisect with a clean run, and locate the exact node before changing anything. Once you have found the boundary that is being crossed, the fix is usually a single flag or a single node update — but only the diagnostic sequence will tell you which one.

References

  • ComfyUI command-line arguments as defined in source: comfy/cli_args.py (authoritative flags for --lowvram, --gpu-only, --cpu-vae, --async-offload, and the fp8/bf16 precision flags).
  • PyTorch device semantics for the .to() method and tensor placement: torch.Tensor.to.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *