Skip to content

llm_shared/optimization: two implementations of meta-device layer eviction, one hand-rolled #13404

Description

@mrveiss

Summary

There are two independent implementations of "move a transformer layer to the meta device" in autobot-backend/llm_shared/optimization/. One delegates to the supported PyTorch API; the other hand-rolls it, and that hand-rolled copy carried a bug that made layer eviction raise instead of free memory.

Found while fixing layer_inference_test.py for #13162 (PR #13402).

The two implementations

Canonical — meta_eviction.py (#1952):

def _evict_standard_layer(layer, layer_repr):
    layer.to("meta")

plus an accelerate.set_module_tensor_to_device path for quantized modules. This is the supported API and handles parameters, buffers and shared storage correctly.

Hand-rolled — layer_inference.py::_move_to_meta (#1946):

Walks named_parameters() and named_buffers() itself and rebuilds each tensor on the meta device. This reimplements what nn.Module._apply already does.

Why the duplication mattered

The hand-rolled copy did:

param.data = torch.empty(param.shape, dtype=param.dtype, device=meta)

PyTorch rejects a cross-device-type .data assignment with RuntimeError: Attempted to call variable.set_data(tensor), but variable and tensor have incompatible tensor type, because a meta tensor carries a different TensorImpl. LayerInferenceEngine.evict_layer() therefore raised on every call under real PyTorch — the eviction that the whole layer-by-layer mode exists to perform.

nn.Module.to("meta") gets this right precisely because Module._apply checks _has_compatible_shallow_copy_type first and re-registers a new Parameter when the types differ, instead of mutating .data.

PR #13402 fixes the crash in place (re-registering parameters, mirroring the _set_buffer helper the same function already used), because that is the surgical change and the torch-dependent tests can only be verified in CI. It does not consolidate the two implementations.

Proposed work

  • Collapse layer_inference._move_to_meta onto meta_eviction.evict_layer_to_meta, or onto nn.Module.to("meta") directly
  • Confirm the quantized path is reachable from LayerInferenceEngine — meta_eviction has one and layer_inference does not, so a quantized layer evicted through layer_inference may hit an overridden .to()
  • Keep layer_inference_test.py::TestModuleHelpers and meta_eviction_test.py both green against the single implementation

Related: #1946, #1952, #13162, PR #13402

Activity

  1. mrveiss commented on Aug 4, 2026

    @mrveiss
    OwnerAuthor

    Not consolidating this blind — the two implementations are not equivalent

    I picked this up and stopped short of the swap, deliberately.

    The consolidation looks like a one-liner: have layer_inference._move_to_meta delegate to meta_eviction.evict_layer_to_meta. But the two differ in a way that matters for the thing eviction exists to do.

    _evict_standard_layer calls layer.to("meta"), which goes through nn.Module._apply and preserves parameter sharing — two names pointing at one tensor still point at one tensor afterwards.

    _move_to_meta walks named_parameters(remove_duplicate=False) and re-registers every alias separately, deliberately untying shared storage. Its comment says why:

    a missed alias would keep the real tensor — and its memory — alive, defeating the eviction

    So the hand-rolled version is not simply a worse copy of the canonical one; it encodes a different decision about aliased weights. Whether .to("meta") frees the same memory for a layer with tied weights is a question about PyTorch's storage handling that I cannot answer from reading, and cannot test here: neither torch nor accelerate is installed on this box, and layer_inference_test.py is CI-only for that reason.

    Shipping an unverifiable behavioural change to a memory-eviction path, where the failure mode is a silent leak rather than an exception, is worse than leaving a documented duplication. So this stays open with the analysis recorded rather than half-done.

    What would make it safe

    1. Determine whether layer.to("meta") releases storage for tied weights, or whether the alias must be untied first — a short experiment on a machine with torch, using a module with deliberately shared parameters.
    2. If .to("meta") is sufficient: delete _move_to_meta, _set_parameter and _set_buffer, and call evict_layer_to_meta. Keep the tied-weight case as a test.
    3. If it is not: keep both, and replace this issue's premise with a comment in meta_eviction.py explaining why the hand-rolled path exists — so the next person does not "simplify" it either.

    Either outcome ends the duplication. Only one of them can be chosen with evidence, and that evidence needs torch.

    Related: #13495 fixed the bug that was living in the hand-rolled copy (GrowingKVCache dtype coercion was a sibling of this), and PR #13402 first surfaced the divergence.

  2. modified the milestones: Backlog, v0.12.0 on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions