The pull-transport flag check is wired in (rank-gated, off by default, warns when enabled). Both diagnostic checks log that they ran. Test repairs for helpers v0.30.0 renamed. Co-Authored-By: Hinata <hinata@idihore.id> Signed-off-by: MOVZX <movzx@yahoo.com>
564 lines
23 KiB
Python
564 lines
23 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Base class for PLE layers that support cross-process CPU offload.
|
|
|
|
Synchronization between the offload process and the GPU main stream is
|
|
handled by :class:`CpuGpuSemaphore`, which uses CUDA's
|
|
``cuStreamWaitValue32`` / ``cuStreamWriteValue32`` stream memory operations.
|
|
|
|
Decode-step timeline, host-mapped PULL design
|
|
--------------------------------------------------------
|
|
Offload process (CPU, no CUDA) GPU main stream (forward)
|
|
------------------------------ ------------------------------
|
|
forward_impl() on CPU ... other GPU ops ...
|
|
wait host flag == 0 WaitValue32(host flag == 1) <- in graph
|
|
rows -> shared pinned host slot cuMemcpyHtoDAsync(slot -> GPU) <- in graph
|
|
publish fence; host flag = 1 return _gpu_output_buffer[:n]
|
|
... model consumes output ...
|
|
WriteValue32(host flag = 0) <- release_outputs
|
|
|
|
``release_outputs`` performs the sole reset. Safety requires serialized local
|
|
forwards on one ordered model stream: a new transaction cannot begin until the
|
|
prior release has been enqueued. If any prepared forward is abandoned or its
|
|
correspondence becomes uncertain, the worker must terminate; resetting,
|
|
retrying, or continuing could reuse stale rows or overwrite live input.
|
|
"""
|
|
|
|
import contextlib
|
|
import functools
|
|
import threading
|
|
from abc import ABC, abstractmethod
|
|
from collections.abc import Callable, Iterator
|
|
from typing import Any, cast
|
|
|
|
import torch
|
|
from cuda.bindings import driver as cuda_driver
|
|
from cuda.bindings.driver import CUstreamWaitValue_flags
|
|
from torch import nn
|
|
|
|
import vllm.envs as envs
|
|
from vllm.utils.torch_utils import direct_register_custom_op
|
|
|
|
# Module-level flag set to True inside the offload subprocess.
|
|
# Because the offload process and GPU worker processes are separate OS
|
|
# processes (spawned via multiprocessing), each has its own memory space.
|
|
# A plain module-level bool is sufficient -- no thread-local storage needed.
|
|
_offload_worker_flag = False
|
|
|
|
# Native FULL capture runs on one Python thread. The custom-op implementation
|
|
# appends the exact arguments from every wait-copy invocation while that thread
|
|
# is inside the graph manager's capture scope.
|
|
_ple_copy_capture = threading.local()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def observe_ple_copy_geometry() -> Iterator[list[tuple[str, int, int]]]:
|
|
"""Collect ``(layer, rows, bytes)`` from actual wait-copy executions."""
|
|
if getattr(_ple_copy_capture, "records", None) is not None:
|
|
raise RuntimeError("nested PLE copy-geometry observation is unsupported")
|
|
records: list[tuple[str, int, int]] = []
|
|
_ple_copy_capture.records = records
|
|
try:
|
|
yield records
|
|
finally:
|
|
del _ple_copy_capture.records
|
|
|
|
|
|
def _record_ple_copy_geometry(
|
|
layer_name: str,
|
|
num_tokens: int,
|
|
row_bytes: int,
|
|
) -> None:
|
|
records = getattr(_ple_copy_capture, "records", None)
|
|
if records is not None:
|
|
rows = int(num_tokens)
|
|
records.append((str(layer_name), rows, rows * int(row_bytes)))
|
|
|
|
|
|
def is_offload_process() -> bool:
|
|
"""Return True inside the dedicated PLE CPU-offload subprocess."""
|
|
return _offload_worker_flag
|
|
|
|
|
|
def mark_as_offload_worker() -> None:
|
|
"""Mark the current process as the dedicated PLE offload worker."""
|
|
global _offload_worker_flag
|
|
_offload_worker_flag = True
|
|
|
|
|
|
def _cuda_check(result: Any, operation: str) -> Any:
|
|
"""Check the ``(CUresult, ...)`` tuple returned by cuda-python calls."""
|
|
error = result[0] if isinstance(result, tuple) else result
|
|
if error.value != 0:
|
|
raise RuntimeError(f"{operation} failed: {error}")
|
|
return result
|
|
|
|
|
|
class CpuGpuSemaphore:
|
|
"""Cross-process semaphore backed by a one-element int32 CUDA tensor.
|
|
|
|
The flag is stored in a regular GPU tensor that is shared through
|
|
PyTorch's CUDA IPC mechanism. Both processes obtain device pointers that
|
|
map to the same physical GPU memory, so a stream-memory wait in the GPU
|
|
worker observes the write issued by the offload process.
|
|
"""
|
|
|
|
RESET_VALUE = 0
|
|
DONE_VALUE = 1
|
|
|
|
def __init__(self, device: torch.device) -> None:
|
|
self._flag_tensor = torch.zeros(1, dtype=torch.int32, device=device)
|
|
self._flag_ptr: int | None = None
|
|
|
|
@classmethod
|
|
def from_ipc_tensor(cls, flag_tensor: torch.Tensor) -> "CpuGpuSemaphore":
|
|
"""Construct a semaphore from a CUDA tensor received through IPC."""
|
|
semaphore = cls.__new__(cls)
|
|
semaphore._flag_tensor = flag_tensor
|
|
semaphore._flag_ptr = None
|
|
return semaphore
|
|
|
|
@classmethod
|
|
def from_host_mapped(
|
|
cls, host_flag_tensor: torch.Tensor, device_ptr: int
|
|
) -> "CpuGpuSemaphore":
|
|
"""Semaphore on a page-locked, device-mapped shared host int32.
|
|
|
|
``host_flag_tensor`` keeps the shared storage alive; ``device_ptr`` is
|
|
its device address (``cuMemHostGetDevicePointer``). Stream memory ops
|
|
on it are executed by the GPU against host memory, so a CPU process
|
|
with no CUDA context can signal it with a plain store.
|
|
"""
|
|
semaphore = cls.__new__(cls)
|
|
semaphore._flag_tensor = host_flag_tensor
|
|
semaphore._flag_ptr = int(device_ptr)
|
|
return semaphore
|
|
|
|
@property
|
|
def flag_tensor(self) -> torch.Tensor:
|
|
"""Return the tensor that backs the semaphore flag."""
|
|
return self._flag_tensor
|
|
|
|
@property
|
|
def flag_ptr(self) -> int:
|
|
"""Device address used by the stream memory operations."""
|
|
if self._flag_ptr is not None:
|
|
return self._flag_ptr
|
|
return self._flag_tensor.data_ptr()
|
|
|
|
def reset(self, stream: torch.cuda.Stream | None = None) -> None:
|
|
"""Enqueue ``WriteValue32(flag=0)`` on ``stream``."""
|
|
if stream is None:
|
|
stream = torch.cuda.current_stream()
|
|
_cuda_check(
|
|
cuda_driver.cuStreamWriteValue32(
|
|
cuda_driver.CUstream(stream.cuda_stream),
|
|
cuda_driver.CUdeviceptr(self.flag_ptr),
|
|
self.RESET_VALUE,
|
|
0,
|
|
),
|
|
"CpuGpuSemaphore.reset",
|
|
)
|
|
|
|
def signal(self, stream: torch.cuda.Stream | None = None) -> None:
|
|
"""Enqueue ``WriteValue32(flag=1)`` on ``stream``."""
|
|
if stream is None:
|
|
stream = torch.cuda.current_stream()
|
|
_cuda_check(
|
|
cuda_driver.cuStreamWriteValue32(
|
|
cuda_driver.CUstream(stream.cuda_stream),
|
|
cuda_driver.CUdeviceptr(self.flag_ptr),
|
|
self.DONE_VALUE,
|
|
0,
|
|
),
|
|
"CpuGpuSemaphore.signal",
|
|
)
|
|
|
|
def wait_reset(self, stream: torch.cuda.Stream | None = None) -> None:
|
|
"""Enqueue ``WaitValue32(flag==0)`` on ``stream``."""
|
|
if stream is None:
|
|
stream = torch.cuda.current_stream()
|
|
_cuda_check(
|
|
cuda_driver.cuStreamWaitValue32(
|
|
cuda_driver.CUstream(stream.cuda_stream),
|
|
cuda_driver.CUdeviceptr(self.flag_ptr),
|
|
self.RESET_VALUE,
|
|
CUstreamWaitValue_flags.CU_STREAM_WAIT_VALUE_EQ.value,
|
|
),
|
|
"CpuGpuSemaphore.wait_reset",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Custom op: vllm::ple_offload_wait
|
|
# ---------------------------------------------------------------------------
|
|
# Wraps cuStreamWaitValue32 as a torch custom op so that
|
|
# torch.compile(fullgraph=True) treats the CUDA Driver call as an opaque node.
|
|
# The hidden_states argument creates a dynamo data-dependency edge that prevents
|
|
# the wait from being reordered before preceding GPU work.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _ple_offload_wait_impl(
|
|
sem_flag_tensor: torch.Tensor,
|
|
gpu_output_buffer: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
) -> None:
|
|
"""Wait for the CPU result without releasing its output buffer."""
|
|
stream = torch.cuda.current_stream()
|
|
cuda_stream = cuda_driver.CUstream(stream.cuda_stream)
|
|
dev_ptr = cuda_driver.CUdeviceptr(sem_flag_tensor.data_ptr())
|
|
_cuda_check(
|
|
cuda_driver.cuStreamWaitValue32(
|
|
cuda_stream,
|
|
dev_ptr,
|
|
CpuGpuSemaphore.DONE_VALUE,
|
|
CUstreamWaitValue_flags.CU_STREAM_WAIT_VALUE_EQ.value,
|
|
),
|
|
"cuStreamWaitValue32(done)",
|
|
)
|
|
|
|
|
|
def _ple_offload_wait_fake(
|
|
sem_flag_tensor: torch.Tensor,
|
|
gpu_output_buffer: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
) -> None:
|
|
"""Represent the side-effect-only wait during dynamo tracing."""
|
|
pass
|
|
|
|
|
|
direct_register_custom_op(
|
|
op_name="ple_offload_wait",
|
|
op_func=_ple_offload_wait_impl,
|
|
mutates_args=["gpu_output_buffer"],
|
|
fake_impl=_ple_offload_wait_fake,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Custom op: vllm::ple_offload_wait_copy (host-mapped pull design)
|
|
# ---------------------------------------------------------------------------
|
|
# The offload process writes the result into a page-locked shared host buffer
|
|
# and stores flag=1 in a page-locked shared host int32; it holds no CUDA
|
|
# context. On the CURRENT stream (captured into the graph like the wait above):
|
|
# cuStreamWaitValue32(host flag == 1) -> GPU polls host memory
|
|
# cuMemcpyAsync(gpu buffer <- host buffer, num_tokens * row_bytes)
|
|
# The copy engine reads host memory coherently after the flag was observed,
|
|
# and the consumers read device memory as before. The flag is NOT reset here:
|
|
# ``release_outputs`` resets it after the forward (unchanged semantics), so
|
|
# capture warm-ups and dummy runs behave exactly as with ``ple_offload_wait``.
|
|
# No CPU tensor and no raw address enters the compiled graph: the op resolves
|
|
# the live pointers by layer name (see _PULL_LAYERS).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Fork (found on the reference host): the host flag / host output pointers and
|
|
# the row size are NOT op arguments any more. As Python ints they were
|
|
# specialised into the compiled graph as constants, so a boot that LOADED the
|
|
# AOT compile cache (no Dynamo pass) waited on the previous process's addresses:
|
|
# cuStreamWaitValue32 -> CUDA_ERROR_INVALID_VALUE on the first forward. The op
|
|
# now carries only process-stable values (buffer, token count, layer name) and
|
|
# resolves the live pointers from this registry on every call.
|
|
_PULL_LAYERS: dict[str, "PleOffloadLayer"] = {}
|
|
|
|
|
|
def _ple_offload_wait_copy_impl(
|
|
gpu_output_buffer: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
num_tokens: int,
|
|
layer_name: str,
|
|
) -> None:
|
|
"""Resolve this process's live pointers for ``layer_name``, then wait+DMA."""
|
|
layer = _PULL_LAYERS.get(layer_name)
|
|
if layer is None or layer._host_output_ptr is None:
|
|
raise RuntimeError(
|
|
f"PLE wait_copy: no armed pull layer named {layer_name!r} in this "
|
|
f"process (armed: {sorted(_PULL_LAYERS)})"
|
|
)
|
|
_ple_offload_wait_copy_raw(
|
|
gpu_output_buffer,
|
|
hidden_states,
|
|
layer._sem.flag_ptr,
|
|
layer._host_output_ptr,
|
|
layer._host_row_bytes,
|
|
num_tokens,
|
|
layer_name,
|
|
)
|
|
|
|
|
|
def _ple_offload_wait_copy_raw(
|
|
gpu_output_buffer: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
flag_ptr: int,
|
|
host_ptr: int,
|
|
row_bytes: int,
|
|
num_tokens: int,
|
|
layer_name: str,
|
|
) -> None:
|
|
"""Wait for the host-side result, then DMA it into the GPU buffer."""
|
|
if not 0 <= int(num_tokens) <= gpu_output_buffer.shape[0] or int(row_bytes) != (
|
|
gpu_output_buffer.shape[1] * gpu_output_buffer.element_size()
|
|
):
|
|
raise RuntimeError(
|
|
f"PLE wait_copy out of bounds: {num_tokens} rows x {row_bytes} B vs "
|
|
f"GPU buffer {tuple(gpu_output_buffer.shape)} {gpu_output_buffer.dtype}"
|
|
)
|
|
nbytes = int(num_tokens) * int(row_bytes)
|
|
_record_ple_copy_geometry(layer_name, num_tokens, row_bytes)
|
|
stream = cuda_driver.CUstream(torch.cuda.current_stream().cuda_stream)
|
|
_cuda_check(
|
|
cuda_driver.cuStreamWaitValue32(
|
|
stream,
|
|
cuda_driver.CUdeviceptr(flag_ptr),
|
|
CpuGpuSemaphore.DONE_VALUE,
|
|
CUstreamWaitValue_flags.CU_STREAM_WAIT_VALUE_EQ.value,
|
|
),
|
|
"cuStreamWaitValue32(host flag done)",
|
|
)
|
|
if nbytes > 0:
|
|
# host_ptr is this worker's host address of page-locked (registered)
|
|
# memory, so the copy is a true async DMA and is capturable.
|
|
_cuda_check(
|
|
cuda_driver.cuMemcpyHtoDAsync(
|
|
cuda_driver.CUdeviceptr(gpu_output_buffer.data_ptr()),
|
|
host_ptr,
|
|
nbytes,
|
|
stream,
|
|
),
|
|
"cuMemcpyHtoDAsync(PLE host -> GPU)",
|
|
)
|
|
|
|
|
|
def _ple_offload_wait_copy_fake(
|
|
gpu_output_buffer: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
num_tokens: int,
|
|
layer_name: str,
|
|
) -> None:
|
|
"""Represent the side-effect-only wait + copy during dynamo tracing."""
|
|
pass
|
|
|
|
|
|
direct_register_custom_op(
|
|
op_name="ple_offload_wait_copy",
|
|
op_func=_ple_offload_wait_copy_impl,
|
|
mutates_args=["gpu_output_buffer"],
|
|
fake_impl=_ple_offload_wait_copy_fake,
|
|
)
|
|
|
|
|
|
class PleOffloadLayer(nn.Module, ABC):
|
|
"""Base class for embedding-like PLE layers that can run on CPU.
|
|
|
|
Subclasses implement :meth:`forward_impl`, which contains the regular
|
|
on-device computation. In a GPU worker with PLE offload enabled, subclass
|
|
constructors are skipped so their large weights are never allocated. The
|
|
CPU process constructs a complete copy and owns those weights instead.
|
|
"""
|
|
|
|
_is_cpu_offloaded = False
|
|
_gpu_output_buffer: torch.Tensor
|
|
_sem: CpuGpuSemaphore
|
|
# Host-mapped pull design: host address of the shared output buffer and
|
|
# the bytes per output row. ``None`` keeps the IPC push behaviour.
|
|
_host_output_ptr: int | None = None
|
|
_host_row_bytes: int = 0
|
|
_ple_offload_name: str = "<unbound>"
|
|
|
|
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
"""Skip subclass initialization in cross-process GPU-worker mode."""
|
|
super().__init_subclass__(**kwargs)
|
|
original_init_obj = cls.__dict__.get("__init__")
|
|
if original_init_obj is None or not callable(original_init_obj):
|
|
return
|
|
original_init = cast(Callable[..., None], original_init_obj)
|
|
|
|
@functools.wraps(original_init)
|
|
def guarded_init(
|
|
self: "PleOffloadLayer", *args: object, **kwargs: object
|
|
) -> None:
|
|
if envs.VLLM_E2_PLE_PULL_TRANSPORT and not is_offload_process():
|
|
nn.Module.__init__(self)
|
|
return
|
|
original_init(self, *args, **kwargs)
|
|
|
|
cls.__init__ = guarded_init # type: ignore[method-assign, assignment]
|
|
|
|
@classmethod
|
|
def get_target_device(cls) -> torch.device:
|
|
"""Return CPU for the offload process and the active GPU otherwise."""
|
|
if envs.VLLM_E2_PLE_PULL_TRANSPORT:
|
|
return torch.device("cpu")
|
|
return torch.device("cuda", torch.accelerator.current_device_index())
|
|
|
|
@abstractmethod
|
|
def forward_impl(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
input_ids: torch.Tensor,
|
|
*args: object,
|
|
**kwargs: object,
|
|
) -> torch.Tensor:
|
|
"""Execute the actual embedding computation on the owning device."""
|
|
raise NotImplementedError
|
|
|
|
def get_offload_output_dtype(self, default_dtype: torch.dtype) -> torch.dtype:
|
|
"""Return the dtype used by cross-process output buffers."""
|
|
return default_dtype
|
|
|
|
def get_offload_output_dim(self, default_dim: int) -> int:
|
|
"""Return the last dimension used by cross-process output buffers."""
|
|
return default_dim
|
|
|
|
def initialize_dummy_offload_metadata(self, device: torch.device) -> None:
|
|
"""Initialize metadata needed by dummy offload execution."""
|
|
|
|
def setup_cross_process_offload(
|
|
self,
|
|
gpu_output_buffer: torch.Tensor,
|
|
semaphore: CpuGpuSemaphore,
|
|
host_output_ptr: int | None = None,
|
|
host_row_bytes: int = 0,
|
|
) -> None:
|
|
"""Configure the GPU-worker placeholder with its offload resources."""
|
|
self._is_cpu_offloaded = True
|
|
self._gpu_output_buffer = gpu_output_buffer
|
|
self._sem = semaphore
|
|
self._host_output_ptr = host_output_ptr
|
|
self._host_row_bytes = int(host_row_bytes)
|
|
if host_output_ptr is not None:
|
|
# Hardening: the registry key is the name the
|
|
# traced graph carries, so it must be a real, unique layer name.
|
|
name = self._ple_offload_name
|
|
if name == "<unbound>":
|
|
raise RuntimeError(
|
|
"PLE pull: setup_cross_process_offload on a layer with no "
|
|
"_ple_offload_name; the op would resolve nothing by name"
|
|
)
|
|
prior = _PULL_LAYERS.get(name)
|
|
if prior is not None and prior is not self:
|
|
raise RuntimeError(
|
|
f"PLE pull: two layers armed under the name {name!r}; the op "
|
|
"would resolve the wrong layer's pointers"
|
|
)
|
|
_PULL_LAYERS[name] = self
|
|
|
|
def forward(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
input_ids: torch.Tensor,
|
|
*args: object,
|
|
**kwargs: object,
|
|
) -> torch.Tensor:
|
|
"""Wait for an offloaded result or delegate to ``forward_impl``."""
|
|
if self._is_cpu_offloaded:
|
|
if self._host_output_ptr is not None:
|
|
torch.ops.vllm.ple_offload_wait_copy(
|
|
self._gpu_output_buffer,
|
|
hidden_states,
|
|
input_ids.shape[0],
|
|
self._ple_offload_name,
|
|
)
|
|
else:
|
|
torch.ops.vllm.ple_offload_wait(
|
|
self._sem.flag_tensor,
|
|
self._gpu_output_buffer,
|
|
hidden_states,
|
|
)
|
|
return self._gpu_output_buffer[: input_ids.shape[0]]
|
|
return self.forward_impl(hidden_states, input_ids, *args, **kwargs)
|
|
|
|
def release_offloaded_output(
|
|
self,
|
|
stream: torch.cuda.Stream | None = None,
|
|
) -> None:
|
|
"""Mark the cross-process output buffer reusable on ``stream``."""
|
|
if self._is_cpu_offloaded:
|
|
self._sem.reset(stream)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pre-launch staleness check for the pull transport (#53899 class)
|
|
# ---------------------------------------------------------------------------
|
|
# WHAT IT TESTS. The transport's correctness rests on one precondition: when a
|
|
# real request is published, every layer's semaphore must be CLEAR, so the
|
|
# ``cuStreamWaitValue32(flag == DONE_VALUE)`` inside the forward (line 279 for
|
|
# the pull path, line 215 for the IPC push path) can only be satisfied by the
|
|
# copy THIS step publishes. If a leftover ``DONE_VALUE`` survives into a real
|
|
# step, that wait is satisfied immediately by the PREVIOUS step's signal and
|
|
# the forward consumes the previous step's PLE rows. That is vllm#53899 on
|
|
# GB10/sm_121 exactly: "every forward consumes the previous step's PLE
|
|
# outputs", for the life of the server, with ``cudagraph_mode=NONE`` correct.
|
|
#
|
|
# In this tree the precondition is held structurally -- the dummy signal is
|
|
# consumed by the dummy forward, ``_release_outputs_impl`` resets every layer
|
|
# and synchronizes the model stream while ``_after_dummy`` is set, and the
|
|
# dummy->real transition takes a TP barrier -- but nothing ever READ the flag
|
|
# back, so the structural argument had no instrument. This is that instrument.
|
|
#
|
|
# WHY NOT AN EPOCH WORD. The stronger fix is a monotone epoch: the launch
|
|
# writes ``epoch = step``, the worker signals ``epoch``, and the placeholder
|
|
# waits ``EQ(epoch)``, so a stale signal carries the previous epoch and cannot
|
|
# satisfy the wait. It is not implementable on this transport as it stands.
|
|
# The compare value is an IMMEDIATE OPERAND of ``cuStreamWaitValue32``
|
|
# (``_ple_offload_wait_copy_impl``, this file, line 279), and that call is
|
|
# issued inside the model forward, which is captured: PIECEWISE and
|
|
# FULL_AND_PIECEWISE both bake the wait into the graph with the value frozen at
|
|
# capture time. Making the compare value change per replay needs the captured
|
|
# node's parameters rewritten before each launch
|
|
# (``cuGraphExecBatchMemOpNodeSetParams``); nothing in this tree's capture path
|
|
# exposes the graph exec handle to the connector, and the wait is reached
|
|
# through a torch custom op inside a compiled region. An epoch word would
|
|
# therefore work only with ``cudagraph_mode=NONE``, i.e. only in the
|
|
# configuration where #53899 does not reproduce. So: binary flag, read back.
|
|
#
|
|
# The check is HOST-SIDE and needs a stream synchronize to be ordered against
|
|
# a pending reset, so it is never run inside a capture and is off by default.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def flag_check_is_authoritative(tp_rank: int) -> bool:
|
|
"""Whether ``flag == RESET_VALUE`` before a launch is an INVARIANT here.
|
|
|
|
Only on the publishing rank (TP0). Why: the offload worker fans a
|
|
result out to every registered TP target, each gated only on that target's
|
|
own flag having been reset, so a non-publishing rank's flag can legitimately
|
|
carry THIS step's signal before that rank reaches its own pre-launch check.
|
|
Reading a set flag there and calling it stale terminates a healthy run.
|
|
|
|
On TP0 the invariant holds by construction: the worker learns of a request
|
|
only from ``_launch``, which runs after the check, and the progress monitor
|
|
refuses overlapping serving submissions, so the previous transaction has
|
|
been released and (after a stream synchronize) its reset has retired.
|
|
|
|
A non-publishing rank cannot tell "this step's fan-out arrived early" from
|
|
"last step's signal was never consumed", because the flag is BINARY and
|
|
carries no transaction identity -- the same representational limit that
|
|
rules out an epoch word. So those ranks are skipped, not checked weakly.
|
|
"""
|
|
return tp_rank == 0
|
|
|
|
|
|
def stale_offload_flags(
|
|
layers: "dict[str, PleOffloadLayer]",
|
|
) -> list[tuple[str, int]]:
|
|
"""Return ``(layer_name, flag_value)`` for every layer whose flag is set.
|
|
|
|
An empty list is the precondition holding. A non-empty list means at least
|
|
one semaphore still carries a signal that no forward consumed, so the next
|
|
``wait`` would be satisfied by the previous step's copy.
|
|
|
|
Pure and side-effect free: the caller owns the ordering (synchronize the
|
|
model stream first, and never call this inside a stream capture). Layers
|
|
that are not cross-process offloaded have no semaphore and are skipped.
|
|
"""
|
|
stale: list[tuple[str, int]] = []
|
|
for name, layer in layers.items():
|
|
if not getattr(layer, "_is_cpu_offloaded", False):
|
|
continue
|
|
value = int(layer._sem.flag_tensor.reshape(-1)[0].item())
|
|
if value != CpuGpuSemaphore.RESET_VALUE:
|
|
stale.append((name, value))
|
|
return stale
|