Tag: Hugging Face

  • Troubleshooting ‘ValueError: Some modules are dispatched on the CPU or the disk’ in Transformers

    Troubleshooting ‘ValueError: Some modules are dispatched on the CPU or the disk’ in Transformers

    TL;DR

    • The dispatch error acts as a safety check when quantized models cannot fit entirely on GPUs because low-precision kernels cannot natively execute on the CPU or disk.
    • Past memory calculation bugs in Accelerate integrations—such as under-sized leaf module buffers and assuming uniform layer sizes in no-split modules—previously caused CPU offloading even on systems with ample GPU VRAM.
    • These issues (tracked in issue #46823) were resolved in Transformers through PR #47203 and PR #47211, superseding earlier proposals such as PR #47061.

    Symptom and Trigger: When the Quantization Dispatch Error Occurs

    In Hugging Face Transformers (working alongside Accelerate and bitsandbytes), the dispatch error occurs during model initialization via from_pretrained() when attempting to load a quantized model—such as with load_in_8bit or load_in_4bit—under automatic or custom device placement using device_map (such as device_map="auto").

    When this condition is met, Hugging Face Transformers raises the following exception:

    ValueError: Some modules are dispatched on the CPU or the disk. Make sure you have enough GPU RAM to fit the quantized model.
    

    This error serves as a safety check triggered by the following sequence of constraints and runtime checks:

    • Kernel Device Constraints: The underlying quantization engine (bitsandbytes) executes its low-precision matrix operations using specialized GPU kernels.
    • VRAM Exhaustion and Placement Calculation: When the dispatch planner calculates module placement across available hardware, it may determine that available GPU VRAM—after accounting for CUDA kernel context overhead—is insufficient to host every quantized layer.
    • Automatic Fallback: Upon exhausting GPU VRAM, the dispatch planner attempts CPU offloading ("cpu") or disk offloading ("disk") for the overflow layers.
    • Execution Safety Check: Because quantized modules cannot execute their low-precision kernels natively on CPU or disk without explicit offload configurations, Hugging Face Transformers halts execution with ValueError: Some modules are dispatched on the CPU or the disk. Make sure you have enough GPU RAM to fit the quantized model to prevent immediate failure during forward passes or inference.

    The error directly indicates that the planned quantization configuration cannot fit entirely within the targeted GPU VRAM under the current device_map strategy.

    Sources: huggingface error2 #error – Qiita, PR #31098 Enable BNB multi-backend support – SemanticDiff, Bitsandbytes · Hugging Face, Bitsandbytes · Hugging Face, Loading big models into memory · Hugging Face

    Accelerate Memory Calculation Bugs: Why Ample VRAM Models Still Spill to CPU

    While this dispatch error is designed to catch genuine out-of-memory conditions, models loaded with load_in_4bit and device_map="auto" historically dispatched modules to CPU despite having an explicit and ample max_memory configuration. Under these circumstances, ValueError: Some modules are dispatched on the CPU or the disk. Make sure you have enough GPU RAM to fit the quantized model was raised from quantizer_bnb_4bit.validate_environment (via _get_device_map in integrations/accelerate.py).

    This behavior, tracked in issue #46823, stemmed from how device memory caps and layer sizes were calculated in integrations/accelerate.py:

    • Leaf module buffer under-sizing: get_balanced_memory calculates device caps with a buffer that previously only measured _no_split_modules. However, childless leaf modules (such as embed_tokens and lm_head, which are not reduced by quantization) act as un-splittable atoms. When a leaf module was larger than the balanced cap, device placement skipped earlier GPUs and collapsed onto the remaining capacity, spilling excess modules to CPU offloading even when total GPU VRAM was six times the model size.
    • Variable layer sizes in no-split modules: The block size for _no_split_modules was historically computed by sampling only the first matching layer. This caused under-packing when layers of the same class varied significantly in size. For instance, loading MiniMaxAI/MiniMax-M3 (a 428B sparse MoE VL model) with load_in_4bit, device_map="auto", and max_memory={i: "165GiB" for i in range(8)} on an 8x B200 setup triggered the dispatch ValueError because auto-placement under-packed the variable layers.

    While an initial proposal (PR #47061) attempted to size buffers via get_max_layer_size, maintainers determined it was not the correct fix and closed it in favor of targeted solutions that have since been merged into transformers main:

    • PR #47203 (merged July 9, 2026) addressed the variable layer sizing bug in _no_split_modules.
    • PR #47211 (merged September 18, 2026) corrected the leaf-module buffer calculation by adopting an updated sizing formula: buffer = int(1.25 * max(buffer, mean_leaves, max(leave_modules_sizes.values(), default=0))).

    Sources: Fix device_map="auto" under-packing that spills quantized models to cpu (#46823), 4-bit load_in_4bit + device_map='auto' + explicit max_memory still dispatches modules to CPU on a model that fits -> ValueError

    Closing thoughts

    While Transformers frames this error as a straightforward physical memory shortage, the underlying cause in certain multi-GPU quantized workflows was rooted in placement calculations. The handling of variable-sized MoE layers and unquantized leaf modules previously caused device_map="auto" to bypass available GPUs and spill modules to CPU. These issues have since been officially resolved in Transformers through PR #47203 and PR #47211. Developers working on older versions who encounter this error on ample hardware should update their transformers installation or inspect per-module allocations and explicit device maps.

    Frequently Asked Questions

    What causes the ‘ValueError: Some modules are dispatched on the CPU or the disk’ error in Hugging Face Transformers?

    This error occurs during model initialization with quantization settings like load_in_8bit or load_in_4bit when available GPU VRAM is insufficient to hold all layers. Because quantized modules require specialized GPU kernels and cannot execute natively on CPU or disk, Transformers halts execution as a safety check when modules are offloaded.

    Why did this dispatch error occur on systems with ample GPU VRAM?

    Historically, memory calculation bugs in Accelerate integrations caused auto-placement to under-pack GPUs. If variable-sized layers or un-splittable leaf modules exceeded the calculated balanced memory buffer, earlier GPUs were skipped, causing remaining layers to collapse onto the last GPU or spill to CPU.

    Which types of modules commonly triggered unintended CPU offloading during quantization?

    Childless leaf modules not reduced by quantization (such as embed_tokens and lm_head) and variable-sized layers in no-split modules (such as those in sparse MoE architectures) commonly caused under-packing when buffer reservations were under-calculated.

    How were these quantization memory calculation issues fixed in Transformers?

    An early proposed PR (#47061) was superseded by two merged fixes: PR #47203 resolved the variable layer size estimation in no-split modules, and PR #47211 updated the buffer calculation in get_balanced_memory using leaf module sizes (buffer = int(1.25 * max(buffer, mean_leaves, max(leave_modules_sizes.values(), default=0)))).


    Keep reading: LLM Inference & Serving

    All guides in this series: LLM Inference & Serving Guides: vLLM, Ollama & Hugging Face

  • Troubleshooting LangChain OutputParserException: Both Final Answer and Parse-able Action Produced

    Troubleshooting LangChain OutputParserException: Both Final Answer and Parse-able Action Produced

    TL;DR

    • The OutputParserException fires when a ReAct-style agent’s output matches both the action pattern (Action:/Action Input:) and the final-answer prefix (Final Answer:) in the same turn, so ReActSingleInputOutputParser cannot tell whether to dispatch a tool call or end the run.
    • handle_parsing_errors=True catches the exception and, by default, feeds back a fixed placeholder message (“Invalid or incomplete response”) as the next observation rather than the actual error text — pass a custom string or callable instead of True to send back something more specific
    • The most reliable fix is architectural: move off initialize_agent‘s legacy text-parsing ReAct agent onto a native tool-calling agent, where the turn boundary is enforced by the provider’s API instead of a regex over free text.

    Error Symptom: OutputParserException in ReAct Agent Execution

    In a GitHub issue report, a zero-shot ReAct agent built with initialize_agent and AgentType.ZERO_SHOT_REACT_DESCRIPTION raised langchain_core.exceptions.OutputParserException: Parsing LLM output produced both a final answer and a parse-able action while running against a self-hosted Meta-Llama-3-8B-Instruct model through a Hugging Face pipeline, with a Wikipedia search tool and a DuckDuckGo search tool attached.

    The reporter’s code, quoted as posted in the issue:

    runAgent = initialize_agent(
        llm=hf,
        tools=tools,
        verbose=True,
        agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    )
    userInput = "Bitcoin"
    output = runAgent.run(f'Write an academic abstract about {userInput}')
    

    This is reproduced verbatim from the report — hf and tools are defined earlier in the reporter’s own script and not shown here. Note that initialize_agent‘s formal parameter for selecting an agent type is agent, not agent_type. However, when both agent and agent_path are None, initialize_agent defaults to AgentType.ZERO_SHOT_REACT_DESCRIPTION. Thus, passing agent_type is ignored as an extra keyword argument, but the resulting instance is still a zero-shot ReAct agent and reproduces the exact same error.

    When the model over-generates — for example continuing past a tool call to also emit Final Answer: in the same response, or fabricating its own Observation: lines and looping through several invented steps — ReActSingleInputOutputParser.parse() finds both an action match and the final-answer prefix in the same text and raises the exception instead of guessing which one the model meant.

    After first hitting this, the reporter tried passing handle_parsing_errors=True. Rather than resolving cleanly, the run continued generating further hallucinated Thought/Action/Action Input/Observation cycles on its own without reaching a stop state — handle_parsing_errors=True catches the exception and passes the error back into the scratchpad, but it does not prevent the model from continuing to over-generate. A later comment on the same issue suggested adding early_stopping_method="force":

    runAgent = initialize_agent(
        llm=hf,
        tools=tools,
        verbose=True,
        early_stopping_method="force",
        handle_parsing_errors=True,
        agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    )
    

    "force" is already AgentExecutor‘s default value for early_stopping_method. The early_stopping_method parameter is independent of handle_parsing_errors and controls how the agent terminates once max_iterations or max_execution_time is reached (e.g., when repeated parsing retries exhaust the iteration limit), not at the moment the parsing conflict happens.

    Sources: OutputParserException: Parsing LLM output produced both a final answer and a parse-able action

    Root Cause: Turn Ambiguity in ReActSingleInputOutputParser Protocols

    LangChain’s text-based ReAct agent protocol relies on a strict turn-by-turn structure in which tool execution and agent termination are meant to be mutually exclusive:

    • Action step: the model emits Thought:, Action: <tool_name>, and Action Input: <input>, and the runtime is meant to stop there, dispatch an AgentAction, run the tool, and inject the result as the next Observation:.
    • Termination step: the model emits Thought: and Final Answer: <response>, and the runtime returns an AgentFinish to end the loop.

    ReActSingleInputOutputParser (and the legacy MRKL parser) decide which of these happened by testing the model’s raw text for two separate patterns: an action match (Action: ... / Action Input: ...) and the final-answer prefix defined by the FINAL_ANSWER_ACTION constant ("Final Answer:"). The exception fires whenever both patterns match in the same output, which happens in at least two distinct ways:

    1. The model hallucinates the rest of the trajectory. After an Action/Action Input pair, the model keeps generating instead of stopping — inventing its own Observation: text for a tool it never actually called, then continuing through further Thought/Action cycles until it eventually appends a Final Answer: as well.
    2. The model appends a final answer directly, with no fabricated observation in between. The model emits Action:/Action Input: and then, in the same continuous response, immediately adds Thought: I now know the final answer and Final Answer: — without ever generating an Observation: line at all.

    The distinction matters for choosing a fix: anything that stops generation at the Observation: boundary only intercepts case 1, because case 2 never produces that string in the first place. In both cases, because the runtime cannot tell from the text alone whether to dispatch an AgentAction or return an AgentFinish, ReActSingleInputOutputParser aborts and raises langchain_core.exceptions.OutputParserException using the string constant FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE.

    Sources: langchain.schema.output_parser.OutputParserException · Issue #8226 · langchain-ai/langchain · GitHub, vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGQ6Yvdmo…, エラー Parsing LLM output produced both a final answer and a parse-able action #LangChain – Qiita, FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE, Observation: Invalid or incomplete response using HF TGI · Issue #9381 · langchain-ai/langchain · GitHub

    How to Fix It: From Quick Patch to Eliminating the Error Class

    There are three points at which you can intervene, from patching around the crash to removing the ambiguity altogether.

    1. Catch the exception with handle_parsing_errors. Passing handle_parsing_errors=True to initialize_agent/AgentExecutor stops OutputParserException from crashing the run by catching the exception and returning the error text (or a fallback like “Invalid or incomplete response”) back as an observation so the model can retry. Passing a custom string (e.g. handle_parsing_errors="<custom message>") or a callable lets you control the instructional prompt or dynamically sanitize the output. On its own this only manages the failure at runtime — it does not prevent the model from over-generating again on a later turn. If the model continually repeats parsing errors until hitting max_iterations or max_execution_time, early_stopping_method determines how the agent handles termination at that limit.

    2. Verify and configure stop sequences for the Observation: boundary. Zero-shot ReAct agents in LangChain generally pass stop sequences such as ["\nObservation:", "\n\tObservation:"] to LLM invocations to halt generation immediately after an action is emitted. However, local model wrappers (such as Hugging Face pipeline wrappers) or certain model setups may fail to apply or honor these stop sequences properly. Verifying that stop arguments are actually respected by your LLM wrapper prevents case 1 — the model hallucinating its own observations and looping through further steps. Note that stop sequences do not prevent case 2, where the model appends Final Answer: directly after Action Input: without ever generating an Observation: token.

    3. Move to a native tool-calling agent. ReActSingleInputOutputParser and other text-based parsers exist only because the ReAct protocol encodes actions and answers as plain text that has to be pattern-matched after the fact. Where the underlying model supports tool calling, building the agent with create_tool_calling_agent (or an equivalent structured-output agent) instead of initialize_agent‘s ZERO_SHOT_REACT_DESCRIPTION moves argument parsing and turn boundaries into the API itself, so there is no free-text Action:/Final Answer: pattern left to collide. This isn’t guaranteed for every model, though — a self-hosted model served through a Hugging Face pipeline, like the Meta-Llama-3-8B-Instruct example above, may not expose tool calling the way a hosted provider API does. It is also worth knowing that initialize_agent/AgentExecutor is LangChain’s older agent interface; current LangChain documentation points toward create_agent/LangGraph-based agents as the actively maintained path going forward.

    Sources: langchain.schema.output_parser.OutputParserException · Issue #8226 · langchain-ai/langchain · GitHub, FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE

    Closing thoughts

    This parsing error illustrates the limitations of relying on regex-based text parsing for agent control flow when underlying LLMs generate past expected turn boundaries. While parameters like handle_parsing_errors handle parser exceptions at runtime by passing feedback back to the scratchpad, and early_stopping_method dictates how the run concludes when iteration limits are reached, neither eliminates the root formatting conflicts on its own.

    For more robust agent pipelines, using native tool-calling agents where supported offers a cleaner architecture by moving tool dispatching from free-form prompt text to structured API interactions.

    Frequently Asked Questions

    Why does the OutputParserException regarding a final answer and parse-able action occur in LangChain?

    This exception occurs when an LLM produces both an action pattern (like Action: and Action Input:) and a final answer prefix (Final Answer:) in a single output turn. Because the ReAct parser checks for both conditions and finds both to be true, it cannot determine whether to invoke a tool via an AgentAction or end execution with an AgentFinish, causing it to abort and raise the error.

    What issue can arise when only handle_parsing_errors=True is passed to initialize_agent?

    Passing handle_parsing_errors=True catches the OutputParserException and returns the error message (or a fallback like “Invalid or incomplete response”) back into the scratchpad as an observation. However, it does not prevent an over-generating model from continuing to hallucinate Thought/Action/Action Input/Observation cycles on subsequent turns instead of cleanly resolving.

    What is the difference between handle_parsing_errors and early_stopping_method?

    handle_parsing_errors manages runtime parser failures when an OutputParserException is raised by feeding error feedback or a custom observation back to the model. early_stopping_method is an independent setting that controls how the execution terminates once max_iterations or max_execution_time has been reached.

    What is the underlying root cause of turn ambiguity in ReActSingleInputOutputParser?

    The root cause is the LLM over-generating text across turn boundaries rather than yielding control after emitting an action or a final answer. When the model outputs both action patterns and termination keywords in the same turn, the parser’s regex and string checks match both mutually exclusive states.


    Keep reading: LLM Agents & Tooling

    All guides in this series: LLM Agent Guides: LangChain, LangGraph & AI Tooling

  • Fixing Quality Degradation When Merging 4-Bit QLoRA Adapters in PEFT

    Fixing Quality Degradation When Merging 4-Bit QLoRA Adapters in PEFT

    TL;DR

    • In QLoRA, the base model weights are stored in 4-bit precision (NormalFloat4 / NF4 via bitsandbytes) while the trained LoRA adapter matrices stay in 16-bit floating point (torch.float16 or torch.bfloat16).
    • Calling merge_and_unload directly on a model that is still loaded in 4-bit forces PEFT to dequantize each weight to 16-bit, add the adapter delta, and then re-quantize the result back into NF4.
    • The dequantize and add steps are effectively lossless; the final re-quantization is not. It rounds every merged weight to the nearest NF4 bucket, across every layer at once, and PEFT warns this “may get different generations due to rounding errors.”
    • The fix is to load the base model in 16-bit, run merge_and_unload there, save, and — only if deployment needs it — quantize the merged checkpoint once afterward.

    Understanding the Pitfall: Why Merging into 4-Bit Weights Hurts Output Quality

    In QLoRA configurations, the base model weights are stored in 4-bit precision formats such as NormalFloat4 (NF4) via bitsandbytes, whereas the trained LoRA adapter matrices A and B are kept in 16-bit floating-point precision (torch.float16 or torch.bfloat16).

    When you call merge_and_unload() on a model that is still loaded in 4-bit, PEFT cannot add a 16-bit delta to a 4-bit weight directly. Instead it dequantizes each base weight back to the compute dtype (bf16/fp16), adds the LoRA update ΔW = (α / r)·BA in floating point, and then re-quantizes the updated weight back into NF4. The dequantize and add steps are well defined and essentially lossless. The problem is the last step: re-quantization snaps every merged weight to the nearest value its NF4 bucket can represent, and that rounding is applied to every weight in every merged layer at once.

    So there is a single mechanism at work here — re-quantization rounding error — not a pile of unrelated failure modes. NF4 and fp16 are not “incompatible” formats; the conversion between them is well defined. What you lose is precision when a 16-bit sum is forced back into NF4’s limited set of representable values.

    PEFT itself flags this rather than forbidding it: merging a LoRA module into a 4-bit linear layer “may get different generations due to rounding errors.” In practice the merged model still runs, but its outputs can shift measurably from the same adapter running unmerged — enough to matter for evaluation or production, and impossible to recover once the merge is saved. Community threads on the PEFT tracker (issue #2321, issue #2105) are users asking why this warning appears and asking for the behavior to be documented, not reports of the model being destroyed. Treat it as a quality regression to design around, not a guaranteed failure.

    Sources: LoRA · Hugging Face, [Warning] `Merge lora module to 4-bit linear may get different generations` · Issue #2321 · huggingface/peft ·, merge_and_unload docs do not clarify behaviour for quantized base models · Issue #2105 · huggingface/peft · Gi

    The Correct Workflow: Reloading in Full Precision Before Merging

    The officially supported path is to merge the adapter into an unquantized 16-bit base model, never into the 4-bit model you trained against.

    1. Load the base model without quantization. Call transformers.AutoModelForCausalLM.from_pretrained with the compute dtype set to torch.bfloat16 or torch.float16 — use dtype= on current Transformers (torch_dtype= still works but is deprecated) — and omit quantization_config / load_in_4bit entirely for this phase. Set device_map="auto", or an explicit CPU/GPU map if memory is tight.
    2. Attach the trained adapter. peft.PeftModel.from_pretrained(base_model, adapter_path) loads the adapter weights onto the unquantized base.
    3. Call merge_and_unload(). This fuses ΔW = (α / r)·BA into the 16-bit base weights and returns a plain Transformers model with no adapter layers left. Useful arguments: safe_merge=True clones each weight matrix one layer at a time to check for NaN before committing that layer (peak overhead is one layer, not a second full model); progressbar=True shows layer-by-layer progress; adapter_names limits the merge to specific adapters.
    4. Save the merged model. model.save_pretrained(output_dir) writes a standalone 16-bit checkpoint that no longer depends on PEFT or bitsandbytes to load.
    5. Re-quantize only if deployment needs it. If you need a 4-bit or 8-bit artifact for inference, quantize this saved 16-bit checkpoint once, after the merge, with a current post-training method (bitsandbytes, torchao, AWQ, or GPTQ). That keeps quantization to a single, measurable step on the final model instead of one buried inside the merge.

    Sources: LoRA · Hugging Face, Quantization · Hugging Face

    System and Memory Requirements for 16-Bit Model Fusion

    The merge arithmetic itself is cheap. The real constraint is holding the base model in 16-bit, since the whole point of the correct workflow is that it is no longer quantized during the merge.

    At bf16/fp16 you need roughly two bytes per parameter, spread across VRAM and system RAM combined, plus headroom for the adapter and for the single weight matrix safe_merge clones per layer as it checks for NaN (one layer at a time, not a full second model). That is several times the footprint of the 4-bit model you trained with, which is the trade-off for avoiding re-quantization.

    You do not need a single GPU large enough to hold the whole model. device_map="auto" offloads layers to CPU RAM, and because the merge is a one-off operation, running it partly or entirely on CPU is fine — fitting matters far more than speed. If system RAM is also tight, pass an explicit device map that keeps most layers on CPU. Keep load_in_4bit off for this whole phase; the low-bit representation for deployment is produced later, from the saved checkpoint.

    Sources: LoRA · Hugging Face

    Comparing Quality: 4-Bit Merge, 16-Bit Merge, and Runtime QLoRA

    Three configurations are worth keeping distinct when you evaluate the result:

    • Naive 4-bit merge — adapter fused into the 4-bit model, with the re-quantization rounding baked into every layer. It runs, but generations can diverge from the trained adapter and the loss is permanent.
    • Proper 16-bit merge — adapter fused into the unquantized base, then saved. Generations track the unmerged adapter up to ordinary floating-point noise, because no re-quantization happens during the merge.
    • Runtime QLoRA inference — base kept in 4-bit, adapter applied on the fly, nothing merged. This is the natural reference point for adapter quality, but it carries adapter overhead on every forward pass and needs bitsandbytes at serving time.

    This is a conceptual comparison, not a numeric benchmark — the right numbers are the ones you measure on your own task. For deployment, do the 16-bit merge first, then quantize the merged checkpoint if you need a low-bit model for inference. Applying quantization once to a clean merged model (bitsandbytes NF4 for a calibration-free load-time path, or AWQ / GPTQ for calibration-based PTQ) gives you one rounding pass you can actually measure, rather than one hidden inside merge_and_unload. Before shipping, validate the deployed model against the runtime-QLoRA setup on your own evaluation set — a single quantization step on the final model is far easier to sign off on than rounding introduced mid-merge.

    Sources: LoRA · Hugging Face

    Closing thoughts

    Merging a QLoRA adapter straight into 4-bit base weights is not a shortcut worth taking. PEFT will do it, but it has to re-quantize every updated weight back into NF4, and that rounding pass — applied across every merged layer — is what pushes the fused model’s outputs away from what you trained.

    The reliable path is boring: load the base model in 16-bit, attach the adapter, run merge_and_unload there, save, and quantize once at the end if deployment needs it. That keeps quantization to a single measurable step on the final model instead of an invisible one inside the merge. When you do need a low-bit artifact, reach for a current post-training quantization method rather than merging inside an already-quantized model.

    Frequently Asked Questions

    What precisions are used for the base model and adapters in QLoRA?

    In QLoRA, the base model weights are stored in 4-bit precision formats like NormalFloat4 (NF4) via bitsandbytes. The trained LoRA adapter matrices are kept in 16-bit floating-point precision, such as torch.float16 or torch.bfloat16.

    Why does calling merge_and_unload directly on a 4-bit model change generation quality?

    PEFT cannot add a 16-bit adapter delta to a 4-bit weight, so it dequantizes each base weight to bf16/fp16, adds the delta, and re-quantizes the result back into NF4. That final re-quantization rounds every merged weight to the nearest NF4 bucket, across every layer at once, and PEFT warns it “may get different generations due to rounding errors.” The dequantize and add steps are lossless; only the re-quantization loses information.

    What stops the LoRA update from integrating exactly during a 4-bit merge?

    Nothing stops the addition itself — it happens in floating point and is accurate. What you lose is precision when the summed weight is forced back into NF4’s limited set of representable values. Merging into an unquantized 16-bit base skips that step entirely, so the adapter integrates without rounding loss.


    Keep reading: Training & Architectures

    All guides in this series: LLM Training & Architecture Guides: Fine-Tuning, RL & New Models

  • Fixing Cannot Copy Out of Meta Tensor Errors in Hugging Face Transformers

    Fixing Cannot Copy Out of Meta Tensor Errors in Hugging Face Transformers

    TL;DR

    • The ‘Cannot copy out of meta tensor’ error happens when attempting manual operations like .to(device) on meta tensors, which serve as memory-free model placeholders.
    • Avoid calling .to() manually when loading models with device_map=’auto’, as Hugging Face Accelerate automatically handles layer placement and device transfers via hooks.
    • To resolve meta tensor errors in custom pipelines, use load_checkpoint_and_dispatch() when dispatching an Accelerate-managed model, or torch.nn.Module.to_empty() when manually allocating a model before loading a state dict — they solve different scenarios, not interchangeable alternatives.

    Understanding Meta Tensors in PyTorch and Hugging Face

    The error NotImplementedError: Cannot copy out of meta tensor; no data! occurs when PyTorch, Hugging Face Transformers, or Accelerate attempts to copy data, perform operations, or execute standard .to(device) calls on a tensor residing on the meta device. In PyTorch, a tensor on the meta device acts as a lightweight placeholder that stores metadata—such as shape, strides, and data type—without allocating physical memory or actual storage bytes. Both PyTorch and Hugging Face Transformers use this mechanism to construct large model architectures instantly without overwhelming CPU RAM or GPU VRAM.

    When loading large models in Hugging Face Transformers using device_map="auto" or low_cpu_mem_usage=True, Accelerate initially instantiates the model skeleton on the meta device. Setting low_cpu_mem_usage=True instructs from_pretrained to load the model via meta-device initialization, which prevents full model weights from being loaded into system memory twice. Accelerate then calculates optimal parameter placement across available hardware (splitting across GPUs, CPU RAM, or disk) before populating real weight data. Data-dependent operations that need real bytes—such as .to("cuda") or .item()—fail on a meta tensor because no physical memory has been allocated (though the exact exception differs: .to() raises this article’s Cannot copy out of meta tensor; no data! error, while .item() raises a separate Tensor.item() cannot be called on meta tensors error). Metadata-only operations like .clone(), by contrast, succeed on a meta tensor and simply return another meta tensor. If custom logic or user code tries to interact with or move these placeholder parameters before they are fully materialized into real tensors, PyTorch throws the error.

    To avoid and fix meta tensor errors, follow these standard practices and API guidelines:

    • Avoid Manual .to() Calls on device_map="auto" Models: Do not call model.to("cuda") or model.to(device) after instantiating a model with device_map="auto". Accelerate automatically manages layer movement and execution across devices using PyTorch forward hooks. Calling manual device transfer methods forces PyTorch to attempt copying empty meta tensors, triggering the exception.
    • Use Native Checkpoint Dispatching for Custom Models: When constructing custom model pipelines using init_empty_weights() to route layer creation onto the meta device, load and place real weights using load_checkpoint_and_dispatch() — this function loads a checkpoint from disk and replaces the empty meta tensors with real weight tensors before attaching execution hooks. dispatch_model() is a narrower tool: it only attaches the execution hooks that route each layer’s forward pass to its assigned device, and it expects the model to already hold real (non-meta) weights — calling it alone on a model still on the meta device will not populate those weights.
    • Use torch.nn.Module.to_empty() for Uninitialized Allocation: If manual model allocation is required before loading state dicts into memory, replace standard .to(device) calls with torch.nn.Module.to_empty(). This native PyTorch method transfers a model off the meta device onto a target device (such as CPU or CUDA) and allocates uninitialized memory without trying to read non-existent meta tensor values.
    • Verify Distributed & Offloading Settings: In distributed training or inference environments (such as DeepSpeed ZeRO-3 or FSDP), ensure training arguments do not conflict with model loading options. Passing explicit offloading configurations or disabling incompatible CPU RAM efficient loading flags ensures model weights are fully materialized before training loops begin.

    Sources: docs.pytorch.org/docs/stable/meta.html, How 🤗 Accelerate runs very large models thanks to PyTorch, Loading big models into memory · Hugging Face, DiffusionGemma weights remain on ‘meta’ device with device_map=”auto” · Issue #46566 · huggingface/transformer

    Closing thoughts

    Looking at the underlying cause, the “cannot copy out of meta tensor” error most often traces back to a friction point where legacy PyTorch habits—specifically manual .to(device) transfers—conflict with modern, automated memory management. That said, it is not always user error: Accelerate’s automatic dispatch logic can itself leave some submodules on the meta device for certain model architectures, and a later data-dependent operation (such as the .item() call inside model.generate()) then fails with a related meta-tensor exception — evidence that dispatch-logic bugs, not just manual .to() misuse, are a real source of these errors. Meta tensors are essential lightweight blueprints that enable the loading of massive architectures, but they require developers to trust frameworks like Hugging Face Accelerate to handle parameter placement behind the scenes. Using load_checkpoint_and_dispatch() for Accelerate-managed models, or to_empty() when manually allocating a model before loading a state dict, resolves most instantiation hurdles—though architecture-specific dispatch failures may still require filing an upstream issue. Ultimately, respecting the boundary between placeholder metadata and materialized weights ensures clean, exception-free pipelines without causing system memory exhaustion.

    Frequently Asked Questions

    What causes the “Cannot copy out of meta tensor” error in PyTorch and Hugging Face?

    The error occurs when PyTorch, Hugging Face Transformers, or Accelerate attempts to copy data, perform operations, or execute manual .to(device) calls on a tensor residing on the meta device. Because meta tensors are lightweight placeholders with no physical memory allocated, standard data-copying operations fail.

    How does device_map=”auto” utilize meta tensors when loading models?

    When using device_map="auto" or low_cpu_mem_usage=True, Accelerate initially instantiates the model skeleton on the meta device to prevent loading full model weights into system memory twice. It calculates optimal parameter placement across hardware before populating real weight data.

    Why should manual .to() calls be avoided on models instantiated with device_map=”auto”?

    Accelerate automatically manages layer movement and execution across devices using forward hooks. Calling .to("cuda") or .to(device) manually forces PyTorch to attempt copying empty meta tensors, triggering the exception.

    How can you safely allocate or dispatch custom models created on the meta device?

    For custom models, developers should load and place real weights using load_checkpoint_and_dispatch(), which reads a checkpoint from disk and replaces the empty meta tensors with real weight tensors. dispatch_model() is not a substitute for this — it only attaches execution hooks to a model whose weights are already real, so calling it alone on a model still on the meta device will not load any weights. If manual allocation is required before loading state dicts, torch.nn.Module.to_empty() should be used instead of standard .to(device) calls.


    Keep reading: LLM Inference & Serving

    All guides in this series: LLM Inference & Serving Guides: vLLM, Ollama & Hugging Face