Skip to content

feat(ascend): add disk checkpoint serving patches - #431

Open
wangx700 wants to merge 1 commit into
vllm-project:ascendfrom
wangx700:feat/ascend-disk-checkpoint-patches
Open

wangx700 wants to merge 1 commit into
vllm-project:ascendfrom
wangx700:feat/ascend-disk-checkpoint-patches

Conversation

@wangx700

@wangx700 wangx700 commented Sep 16, 2026

Copy link
Copy Markdown

Summary

Add the serving-side patches required by disk-backed full and delta weight synchronization on Ascend:

  • append the vLLM disk checkpoint loader and its regression coverage to docker/npu_patch/vllm.patch
  • append NPUWorker.pull_weights() to docker/npu_patch/vllm-ascend.patch

The PR is based directly on the current vllm-project/vime:ascend branch and changes only these two patch files.

Dependency

Depends on #413 for the VIME-side disk full/delta weight synchronization flow and its pinned vLLM/vLLM-Ascend revisions. After #413 merges, this branch can be rebased onto the updated ascend branch and merged directly.

Validation

Validated on Ascend NPUs 8-15 with Qwen3-4B, disk transport, num_rollout=10, rollout batch size 4, two samples per prompt, and response length 256. The table excludes the final v10 update because it overlaps Ray/vLLM job teardown; it compares the initial sync and stable v1-v9 updates.

Stage Delta ratio Delta wire Delta update Delta E2E Full ratio Full checkpoint Full update Full E2E
Initial sync - - 5.64s 34.05s - - 6.13s 48.41s
v1 0.56% 0.13 GB 10.19s 28.45s 100% 7.49 GiB 6.59s 31.09s
v2 0.63% 0.14 GB 4.67s 21.66s 100% 7.49 GiB 6.41s 30.36s
v3 0.70% 0.15 GB 4.24s 20.49s 100% 7.49 GiB 7.26s 31.12s
v4 0.60% 0.13 GB 4.98s 28.05s 100% 7.49 GiB 5.83s 26.13s
v5 0.84% 0.17 GB 4.17s 20.98s 100% 7.49 GiB 6.91s 25.66s
v6 0.63% 0.14 GB 4.23s 20.89s 100% 7.49 GiB 6.12s 26.96s
v7 0.61% 0.13 GB 4.34s 21.32s 100% 7.49 GiB 6.59s 28.42s
v8 0.58% 0.13 GB 4.16s 21.11s 100% 7.49 GiB 6.79s 29.80s
v9 0.77% 0.16 GB 4.30s 21.54s 100% 7.49 GiB 6.38s 29.86s
v1-v9 mean 0.66% ~0.14 GB 5.03s - 100% 7.49 GiB 6.54s -
10 rollout steps - - - 238.55s - - - 307.81s

E2E uses perf/step_time and covers rollout, training wait, training, and the update attributed to the step. Delta transfers about 0.66% of the model on average, but vLLM still reloads a reconstructed 7.49 GiB safetensors checkpoint on this NPU path.

Signed-off-by: wangx700 <wangxin700@huawei.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to maintain host-local checkpoints from full and delta weight versions, integrated into the NPU worker. The review feedback highlights critical issues regarding platform compatibility on non-Unix/non-Linux systems, specifically due to the top-level import of fcntl and the use of os.posix_fadvise. Additionally, the feedback identifies a logical bug in rollback scenarios where target_version is less than applied, and suggests adding a unit test to verify rollback behavior.

Comment on lines +240 to +250
+ applied = _read_applied_version(local_checkpoint_dir)
+ floor = applied if applied is not None else 0
+ start = target_version
+ while start > floor and _is_delta(_version_dir(source_dir, start)):
+ start -= 1
+
+ if applied is None or start > applied:
+ seed_dir = base_dir if start == 0 else _version_dir(source_dir, start)
+ _reset_checkpoint(seed_dir, local_checkpoint_dir, start)
+ else:
+ start = applied

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If target_version < applied (a rollback scenario), the current search logic fails because floor is set to applied. Since start is initialized to target_version, the loop while start > floor never runs, leaving start at target_version (which could be a delta version). Then, start > applied is false, so start is set to applied, and no changes are made. This silently leaves the local checkpoint at the newer version while returning success.

To fix this, when target_version < applied, we must search all the way down to 0 to find the correct base full checkpoint, and we must reset the checkpoint if start != applied.

        applied = _read_applied_version(local_checkpoint_dir)
        if applied is not None and target_version >= applied:
            floor = applied
        else:
            floor = 0
        start = target_version
        while start > floor and _is_delta(_version_dir(source_dir, start)):
            start -= 1

        if applied is None or start != applied:
            seed_dir = base_dir if start == 0 else _version_dir(source_dir, start)
            _reset_checkpoint(seed_dir, local_checkpoint_dir, start)
        else:
            start = applied

Comment on lines +206 to +207
+import fcntl
+import glob

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Remove the top-level import fcntl to prevent ModuleNotFoundError on non-Unix platforms (like Windows) when this module is imported (e.g., during testing or static analysis). We can import it dynamically inside _pull_lock instead.

import glob

Comment on lines +303 to +312
+@contextmanager
+def _pull_lock(local_checkpoint_dir: str):
+ sync_dir = os.path.join(local_checkpoint_dir, SYNC_DIR)
+ os.makedirs(sync_dir, exist_ok=True)
+ with open(os.path.join(sync_dir, "lock"), "w") as lock_file:
+ fcntl.flock(lock_file, fcntl.LOCK_EX)
+ try:
+ yield
+ finally:
+ fcntl.flock(lock_file, fcntl.LOCK_UN)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Import fcntl dynamically here with a fallback to support non-Unix platforms where fcntl is not available.

@contextmanager
def _pull_lock(local_checkpoint_dir: str):
    sync_dir = os.path.join(local_checkpoint_dir, SYNC_DIR)
    os.makedirs(sync_dir, exist_ok=True)
    try:
        import fcntl
    except ImportError:
        yield
        return

    with open(os.path.join(sync_dir, "lock"), "w") as lock_file:
        fcntl.flock(lock_file, fcntl.LOCK_EX)
        try:
            yield
        finally:
            with suppress(OSError):
                fcntl.flock(lock_file, fcntl.LOCK_UN)

Comment on lines +335 to +343
+def _drop_page_cache(path: str) -> None:
+ try:
+ file_descriptor = os.open(path, os.O_RDONLY)
+ try:
+ os.posix_fadvise(file_descriptor, 0, 0, os.POSIX_FADV_DONTNEED)
+ finally:
+ os.close(file_descriptor)
+ except OSError:
+ pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

On non-Linux platforms (like macOS or Windows), os.posix_fadvise is not available and calling it will raise an AttributeError. We should check if os has the posix_fadvise attribute before attempting to use it.

def _drop_page_cache(path: str) -> None:
    if not hasattr(os, "posix_fadvise"):
        return
    try:
        file_descriptor = os.open(path, os.O_RDONLY)
        try:
            os.posix_fadvise(file_descriptor, 0, 0, os.POSIX_FADV_DONTNEED)
        finally:
            os.close(file_descriptor)
    except OSError:
        pass

Comment on lines +192 to +193
+ state = json.loads((local_dir / ".weight_sync" / "state.json").read_text())
+ assert state == {"version": "000000"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Add a unit test to verify that rolling back to an older version (e.g., from version 1 to version 0) works correctly and restores the baseline weights.

    state = json.loads((local_dir / ".weight_sync" / "state.json").read_text())
    assert state == {"version": "000000"}


def test_pull_checkpoint_rollback(tmp_path):
    base_dir = tmp_path / "base"
    source_dir = tmp_path / "published"
    local_dir = tmp_path / "local"
    base_dir.mkdir()
    source_dir.mkdir()

    baseline = np.arange(12, dtype=np.float32).reshape(3, 4)
    updated = baseline + 1
    safetensors.numpy.save_file({"weight": baseline}, base_dir / "model.safetensors")
    (base_dir / "config.json").write_text("{}")
    _write_delta(source_dir, 1, baseline, updated, "xor")

    # Pull to version 1
    pull_checkpoint(str(local_dir), str(base_dir), str(source_dir), 1)
    actual = safetensors.numpy.load_file(local_dir / "model.safetensors")
    np.testing.assert_array_equal(actual["weight"], updated)

    # Rollback to version 0
    pull_checkpoint(str(local_dir), str(base_dir), str(source_dir), 0)
    actual = safetensors.numpy.load_file(local_dir / "model.safetensors")
    np.testing.assert_array_equal(actual["weight"], baseline)
    state = json.loads((local_dir / ".weight_sync" / "state.json").read_text())
    assert state == {"version": "000000"}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant