Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import cuda.core
from rapidsmpf.coll import AllGather
from rapidsmpf.config import Options, get_environment_variables
from rapidsmpf.memory.packed_data import PackedData
from rapidsmpf.statistics import Statistics
from rapidsmpf.streaming.core.actor import run_actor_network
Expand Down Expand Up @@ -50,6 +51,39 @@
T = TypeVar("T")


def resolve_rapidsmpf_options(rapidsmpf_options: Options | None) -> Options:
"""
Resolve ``rapidsmpf_options`` and apply cross-frontend defaults.

If ``None`` is passed, constructs an ``Options`` instance from
environment variables. Then applies defaults that should be consistent
across SPMD, Ray, and Dask. Defaults are set via
``Options.insert_if_absent``, so explicit values or environment
variables always take precedence.

Defaults applied:

- ``num_streaming_threads=4``: moderate worker count for the rapidsmpf
streaming runtime, shared across frontends.

Parameters
----------
rapidsmpf_options
Existing options to resolve, or ``None`` to construct from environment
variables.

Returns
-------
Options
Resolved options with cross-frontend defaults applied.
"""
if rapidsmpf_options is None:
rapidsmpf_options = Options(get_environment_variables())

rapidsmpf_options.insert_if_absent({"num_streaming_threads": "4"})
return rapidsmpf_options


@dataclasses.dataclass(frozen=True)
class ClusterInfo:
"""
Expand Down Expand Up @@ -201,6 +235,66 @@ def global_statistics(self, *, clear: bool = False) -> Statistics:
"""
return Statistics.merge(self.gather_statistics(clear=clear))

def _reset(
self,
*,
rapidsmpf_options: Options | None = None,
executor_options: dict[str, Any] | None = None,
engine_options: dict[str, Any] | None = None,
) -> None:
"""
Reset the engine with new options, keeping cluster resources alive.

The following inputs are fixed at construction time and cannot change:
- ``num_ranks``
- ``num_py_executors`` (in ``executor_options``)
- ``hardware_binding`` (in ``engine_options``)
- ``memory_resource_config`` (in ``engine_options``)

Subclasses must override this method. The override should:
1. Raise :class:`RuntimeError` if the engine is already shut down.
2. Call ``super()._reset(...)`` to apply the universal option validation below.
3. Perform the backend-specific rebuild.

Parameters
----------
rapidsmpf_options
New :class:`Options` for each rank's :class:`Context`.
``None`` is treated as an empty dict.
executor_options
New executor options for the polars ``GPUEngine`` layer.
``None`` is treated as an empty dict.
engine_options
New engine options for the polars ``GPUEngine`` layer.
``None`` is treated as an empty dict.

Raises
------
ValueError
If ``executor_options`` or ``engine_options`` contains a
construction-time-only key (see list above), or if a
reserved key is set (via :func:`check_reserved_keys`).
"""
executor_options = executor_options or {}
engine_options = engine_options or {}
check_reserved_keys(executor_options, engine_options)

_disallowed_exec = {"num_py_executors"} & executor_options.keys()
if _disallowed_exec:
raise ValueError(
f"executor_options keys {sorted(_disallowed_exec)} cannot be "
"changed via _reset(). Construct a fresh engine instead."
)
_disallowed_engine = {
"hardware_binding",
"memory_resource_config",
} & engine_options.keys()
if _disallowed_engine:
raise ValueError(
f"engine_options keys {sorted(_disallowed_engine)} cannot be "
"changed via _reset(). Construct a fresh engine instead."
)

def shutdown(self) -> None:
"""
Shut down engine and release all owned resources.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@
import ucxx._lib.libucxx as ucx_api
from rapidsmpf import bootstrap
from rapidsmpf.communicator.ucxx import barrier, get_root_ucxx_address, new_communicator
from rapidsmpf.config import (
Options,
get_environment_variables,
)
from rapidsmpf.config import Options
from rapidsmpf.progress_thread import ProgressThread
from rapidsmpf.rmm_resource_adaptor import RmmResourceAdaptor
from rapidsmpf.streaming.core.context import Context
Expand All @@ -36,6 +33,7 @@
StreamingEngine,
check_reserved_keys,
evaluate_on_rank,
resolve_rapidsmpf_options,
)
from cudf_polars.experimental.rapidsmpf.frontend.hardware_binding import (
HardwareBindingPolicy,
Expand Down Expand Up @@ -294,6 +292,47 @@ def _teardown_worker(
delattr(dask_worker, attr)


def _reset_worker(
rapidsmpf_options_as_bytes: bytes,
*,
uid: str,
dask_worker: distributed.Worker | None = None,
) -> None:
"""
Rebuild the streaming Context with new options.

Must be called collectively on all workers. A barrier ensures no
worker tears down its Context while peers may still be using it.

Parameters
----------
rapidsmpf_options_as_bytes
Serialized :class:`Options` to install.
uid
Cluster instance identifier used to look up the per-worker context.
dask_worker
Injected by ``distributed`` when called via :meth:`distributed.Client.run`.
"""
assert dask_worker is not None
attr = f"_cudf_polars_mp_context_{uid}"
mp_ctx: _WorkerContext | None = getattr(dask_worker, attr, None)
if mp_ctx is None:
raise RuntimeError(f"_reset_worker called before _setup_worker for uid={uid}")
assert mp_ctx.comm is not None
assert mp_ctx.ctx is not None
# Collective: all ranks idle before any rank tears down its Context.
if mp_ctx.comm.nranks > 1:
barrier(mp_ctx.comm)
# Explicit shutdown is thread-affine. ``distributed.worker.run``
# dispatches sync work onto the worker's event-loop thread, which is
# the same thread that built the Context in ``_setup_worker``.
mp_ctx.ctx.shutdown()
mp_ctx.ctx = None
options = Options.deserialize(rapidsmpf_options_as_bytes)
mp_ctx.ctx = Context.from_options(mp_ctx.comm.logger, mp_ctx.mr, options)
rmm.mr.set_current_device_resource(mp_ctx.ctx.br().device_mr)


def _get_statistics(
*, clear: bool, uid: str, dask_worker: distributed.Worker | None = None
) -> tuple[int, Statistics]:
Expand Down Expand Up @@ -563,13 +602,9 @@ def __init__(
"memory_resource_config", None
)

rapidsmpf_options = (
rapidsmpf_options_as_bytes = resolve_rapidsmpf_options(
rapidsmpf_options
if rapidsmpf_options is not None
else Options(get_environment_variables())
)
rapidsmpf_options.insert_if_absent({"num_streaming_threads": "4"})
rapidsmpf_options_as_bytes = rapidsmpf_options.serialize()
).serialize()

# Unique identifier for this cluster instance; namespaces the per-worker
# attribute so multiple DaskEngine contexts can coexist on the same workers.
Expand Down Expand Up @@ -660,6 +695,55 @@ def __init__(
engine_options={**engine_options, "memory_resource": None},
)

def _reset(
self,
*,
rapidsmpf_options: Options | None = None,
executor_options: dict[str, Any] | None = None,
engine_options: dict[str, Any] | None = None,
) -> None:
"""Reset the engine; see :meth:`StreamingEngine._reset` for the contract."""
if self._dask_context is None:
raise RuntimeError("Cannot reset a shut-down engine")
super()._reset(
rapidsmpf_options=rapidsmpf_options,
executor_options=executor_options,
engine_options=engine_options,
)
executor_options = executor_options or {}
engine_options = engine_options or {}

rapidsmpf_options_as_bytes = resolve_rapidsmpf_options(
rapidsmpf_options
).serialize()

ctx = self._dask_context
# Reset all worker Contexts collectively. ``client.run`` blocks
# until every worker's reset returns; the per-worker barrier
# inside :func:`_reset_worker` synchronizes the teardown across
# workers.
ctx.client.run(
functools.partial(_reset_worker, uid=ctx.rapidsmpf_id),
rapidsmpf_options_as_bytes,
)

# Re-run ``StreamingEngine.__init__`` on the existing instance to
# reconfigure the polars ``GPUEngine`` layer (``self.config``,
# ``self.device``, etc.) with the new options. Pass the existing
# ``self._exit_stack`` so any registered callbacks survive.
StreamingEngine.__init__(
self,
nranks=self._nranks,
executor_options={
**executor_options,
"runtime": "rapidsmpf",
"cluster": "dask",
"dask_context": ctx,
},
engine_options={**engine_options, "memory_resource": None},
exit_stack=self._exit_stack,
)

@classmethod
def from_options(
cls,
Expand Down
Loading
Loading