Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ff45cc7
[dist] fix: keep FSDP2 buffers on compute device with CPU offload
YLlllllllllll Jul 23, 2026
a134860
fix: keep FSDP2 offload buffers on compute device
YLlllllllllll Aug 10, 2026
8d1738c
fix: resolve conflicts with upstream main
YLlllllllllll Aug 10, 2026
d12960e
fix: resolve FSDP2 merge conflict with upstream main
YLlllllllllll Aug 10, 2026
1059c6f
chore: align FSDP2 file with upstream main before branch update
YLlllllllllll Aug 10, 2026
e8bc2d6
chore: align CI workflows with upstream main before branch update
YLlllllllllll Aug 10, 2026
1526c68
Merge branch 'main' into fix/fsdp2-cpu-offload-compute-buffers
YLlllllllllll Aug 10, 2026
5fc05b9
fix: reapply FSDP2 CPU-offload fix after syncing upstream main
YLlllllllllll Aug 10, 2026
8eff52e
[dist] merge: keep FSDP2 buffer aliases while absorbing main material…
YLlllllllllll Aug 25, 2026
601ec99
[ci] fix: run DCP checkpointer tests once per workflow
YLlllllllllll Aug 25, 2026
0c299c3
[fsdp, tests] fix: reconcile offload buffer gates with model runtime
YLlllllllllll Oct 9, 2026
9bc6f37
[dist] fix: sync buffer offload with selective recomputation
YLlllllllllll Oct 15, 2026
5654f92
[dist] fix: preserve compute buffers across current-main CPU offload …
YLlllllllllll Oct 20, 2026
bad1835
Merge upstream main and preserve PR #967 coverage
YLlllllllllll Oct 22, 2026
7bc7ccc
Merge upstream main and preserve CPU-offload buffer contracts
YLlllllllllll Oct 23, 2026
804d743
[ci] test: schedule native CPU-offload buffer regression on NPU
YLlllllllllll Oct 23, 2026
425774b
Merge fused load tests while preserving FSDP buffer offload coverage
YLlllllllllll Oct 26, 2026
23a86a0
[checkpoint] fix: preserve shared buffers during HF loading
YLlllllllllll Oct 26, 2026
75ec473
[checkpoint] fix: retain constructor buffers during random initializa…
YLlllllllllll Oct 26, 2026
b53ccf5
[checkpoint] test: cover random buffer materialization on accelerators
YLlllllllllll Oct 26, 2026
8a5d06f
[dist] fix: preserve persistent buffers during random initialization
YLlllllllllll Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions tests/checkpoints/test_random_init_buffers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright 2026 Bytedance Ltd. and/or its affiliates
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Config-derived buffers must survive random initialization from meta."""

import pytest
import torch
from torch import nn

from scripts.ci.accelerator.launch_plan import LaunchSpec
from veomni.distributed.torch_parallelize import _materialize_and_load_weights
from veomni.models.checkpoint.weights import init_empty_weights
from veomni.utils.device import get_device_id, get_device_type, get_torch_device


class BufferModel(nn.Module):
def __init__(self, overwrite=False):
super().__init__()
self.weight = nn.Parameter(torch.empty(2))
self.register_buffer("persistent_value", torch.tensor([2.0, 5.0]))
self.register_buffer("runtime_value", torch.tensor([3.0, 7.0]), persistent=False)
self.register_buffer("optional_value", None)
self.child = nn.Module()
self.child.register_buffer("persistent_value", torch.tensor([11.0, 13.0]))
self.overwrite = overwrite
self.init_calls = 0

def init_weights(self):
self.init_calls += 1
with torch.no_grad():
self.weight.fill_(19)
if self.overwrite:
self.child.persistent_value.fill_(23)


def _check_materialization(device, *, overwrite, skip_hf):
with init_empty_weights():
model = BufferModel(overwrite=overwrite)
_materialize_and_load_weights(
model,
None,
str(device),
should_skip_hf_weight_load=skip_hf,
is_peft_model=False,
adapter_path=None,
broadcast_from_rank0=False,
)
assert model.weight.device.type == device.type
assert model.persistent_value.device.type == device.type
assert model.runtime_value.device.type == device.type
assert model.child.persistent_value.device.type == device.type
assert model.optional_value is None
torch.testing.assert_close(model.runtime_value.cpu(), torch.tensor([3.0, 7.0]), rtol=0, atol=0)
if skip_hf:
# A later DCP load is authoritative for persistent buffers and params.
assert model.init_calls == 0
else:
assert model.init_calls == 1
torch.testing.assert_close(model.weight.cpu(), torch.full((2,), 19.0), rtol=0, atol=0)
torch.testing.assert_close(model.persistent_value.cpu(), torch.tensor([2.0, 5.0]), rtol=0, atol=0)
child = [23.0, 23.0] if overwrite else [11.0, 13.0]
torch.testing.assert_close(model.child.persistent_value.cpu(), torch.tensor(child), rtol=0, atol=0)


@pytest.mark.launch_plan(provider=LaunchSpec(device="cpu"))
@pytest.mark.parametrize("overwrite", [False, True])
@pytest.mark.parametrize("skip_hf", [False, True])
def test_random_init_and_resume_buffer_materialization_cpu(overwrite, skip_hf):
_check_materialization(torch.device("cpu"), overwrite=overwrite, skip_hf=skip_hf)


@pytest.mark.launch_plan(provider=LaunchSpec(device="accelerator"))
def test_random_init_and_resume_buffer_materialization_accelerator():
device_type = get_device_type()
if device_type == "cpu":
pytest.skip("requires an accelerator runtime")
assert get_torch_device().is_available(), "selected accelerator runtime is unavailable"
device = torch.device(device_type, get_device_id())
for overwrite in (False, True):
for skip_hf in (False, True):
_check_materialization(device, overwrite=overwrite, skip_hf=skip_hf)
17 changes: 12 additions & 5 deletions veomni/distributed/torch_parallelize.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def _reset_hf_initialized_flag(module: nn.Module) -> None:
_reset_hf_initialized_flag(child)


def _to_empty_preserving_nonpersistent_buffers(model: nn.Module, device: str) -> None:
def _to_empty_preserving_nonpersistent_buffers(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please simplify _to_empty_preserving_nonpersistent_buffers to only add preserve_persistent_buffers: bool = False on top of main's existing implementation (i.e. iterating module._buffers.keys() when preserve_persistent_buffers=True and module._non_persistent_buffers_set when False), without the buffer_groups alias-tracking machinery.

model: nn.Module, device: str, preserve_persistent_buffers: bool = False
) -> None:
"""Materialize parameters without discarding config-derived buffers.

``init_empty_weights()`` patches ``register_parameter`` only, so a
Expand All @@ -65,11 +67,14 @@ def _to_empty_preserving_nonpersistent_buffers(model: nn.Module, device: str) ->
exposes ``original_inv_freq``. Buffers outside that shape -- Gemma3's
per-layer-type ``{type}_inv_freq`` and its ``embed_scale``, the Omni audio
tower's sinusoidal ``positional_embedding`` -- have nothing else to restore
them. Persistent buffers are left to whichever loader runs next.
them. Persistent buffers are preserved for random initialization and left
to the loader during checkpoint recovery. Shared registration aliases follow
``to_empty`` semantics and are not reconstructed by this helper.
"""
buffers = []
for module in model.modules():
for name in module._non_persistent_buffers_set:
names = module._buffers.keys() if preserve_persistent_buffers else module._non_persistent_buffers_set
for name in names:
buffer = module._buffers.get(name)
if buffer is None:
continue
Expand All @@ -79,7 +84,7 @@ def _to_empty_preserving_nonpersistent_buffers(model: nn.Module, device: str) ->
# No model does this today; say so rather than let ``to_empty()``
# leave uninitialized memory behind unannounced.
logger.warning_rank0(
f"Non-persistent buffer {name!r} on {type(module).__name__} is on meta and cannot be "
f"Buffer {name!r} on {type(module).__name__} is on meta and cannot be "
"preserved across materialization; it will hold uninitialized memory unless init_weights() sets it."
)
continue
Expand Down Expand Up @@ -203,7 +208,9 @@ def _materialize_and_load_weights(
# Preserve non-persistent buffers on the random-init path too: HF's
# ``_init_weights`` recomputes only rope tables shaped the way it expects,
# so the rest would train on whatever ``to_empty()`` left behind.
_to_empty_preserving_nonpersistent_buffers(model, materialize_device)
_to_empty_preserving_nonpersistent_buffers(
model, materialize_device, preserve_persistent_buffers=not should_skip_hf_weight_load
)
_reset_hf_initialized_flag(model)
# Random init is unnecessary when the checkpoint will overwrite every parameter.
if not should_skip_hf_weight_load:
Expand Down
Loading