diff --git a/docs/source/developer_guide/guides/9_control_messages.md b/docs/source/developer_guide/guides/9_control_messages.md index 57e1b54214..8e95fc4492 100644 --- a/docs/source/developer_guide/guides/9_control_messages.md +++ b/docs/source/developer_guide/guides/9_control_messages.md @@ -76,3 +76,24 @@ retrieved_payload = msg.payload() msg_meta == retrieved_payload # True ``` + +### Conversion from `MultiMessage` to `ControlMessage` + +Starting with version 24.06, the `MultiMessage` type will be deprecated, and all usage should transition to `ControlMessage`. Each `MultiMessage` functionality has a corresponding equivalent in `ControlMessage`, as illustrated below. +```python +import cudf +from morpheus.messages import MultiMessage, ControlMessage + +data = cudf.DataFrame() +msg_meta = MessageMeta(data) +``` + +| **Functionality** | **MultiMessage** | **ControlMessage** | +| -------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------- | +| Initialization | `multi_msg = MultiMessage(msg_meta)` | `control_msg = ControlMessage()`
`control_msg.payload(msg_meta)` | +| Get columns from `cudf.DataFrame` | `multi_msg.get_meta(col_name)` | `control_msg.payload().get_data(col_name)` | +| Set columns values to `cudf.DataFrame` | `multi_msg.set_meta(col_name, value)` | `control_msg.payload().set_data(col_name, value)` | +| Get sliced `cudf.DataFrame` for given start and stop positions | `multi_msg.get_slice(start, stop)` | `control_msg.payload().get_slice(start, stop)` | +| Copy the `cudf.DataFrame` for given ranges of rows | `multi_msg.copy_ranges(ranges)` | `control_msg.payload().copy_ranges(ranges)` | + +Note that the `get_slice()` and `copy_ranges()` functions in `ControlMessage` return the `MessageMeta` after slicing, whereas these functions in `MultiMessage` return a new `MultiMessage` instance. \ No newline at end of file diff --git a/morpheus/messages/memory/response_memory.py b/morpheus/messages/memory/response_memory.py index 89bcc2f053..eb4318f928 100644 --- a/morpheus/messages/memory/response_memory.py +++ b/morpheus/messages/memory/response_memory.py @@ -31,7 +31,7 @@ class ResponseMemory(TensorMemory, cpp_class=_messages.ResponseMemory): """Output memory block holding the results of inference.""" def __new__(cls, *args, **kwargs): - morpheus_logger.deprecated_message_warning(logger, cls, TensorMemory) + morpheus_logger.deprecated_message_warning(cls, TensorMemory) return super().__new__(cls, *args, **kwargs) def get_output(self, name: str): diff --git a/morpheus/messages/message_base.py b/morpheus/messages/message_base.py index fd4c2c5b92..3e8a19385f 100644 --- a/morpheus/messages/message_base.py +++ b/morpheus/messages/message_base.py @@ -17,7 +17,12 @@ import functools import typing +from typing_utils import issubtype + +from morpheus import messages from morpheus.config import CppConfig +from morpheus.messages import ControlMessage +from morpheus.utils import logger as morpheus_logger class MessageImpl(abc.ABCMeta): @@ -44,6 +49,10 @@ def __new__(cls, name, bases, namespace, /, cpp_class=None, **kwargs): @functools.wraps(result.__new__) def _internal_new(other_cls, *args, **kwargs): + # Instantiating MultiMessage and its subclasses from Python or C++ will generate a deprecation warning + if issubtype(other_cls, messages.MultiMessage): + morpheus_logger.deprecated_message_warning(other_cls, ControlMessage) + # If _cpp_class is set, and use_cpp is enabled, create the C++ instance if (getattr(other_cls, "_cpp_class", None) is not None and CppConfig.get_should_use_cpp()): return cpp_class(*args, **kwargs) diff --git a/morpheus/messages/multi_response_message.py b/morpheus/messages/multi_response_message.py index 51a81ce39b..de7a2fb881 100644 --- a/morpheus/messages/multi_response_message.py +++ b/morpheus/messages/multi_response_message.py @@ -126,7 +126,7 @@ class MultiResponseProbsMessage(MultiResponseMessage, cpp_class=_messages.MultiR required_tensors: typing.ClassVar[typing.List[str]] = ["probs"] def __new__(cls, *args, **kwargs): - morpheus_logger.deprecated_message_warning(logger, cls, MultiResponseMessage) + morpheus_logger.deprecated_message_warning(cls, MultiResponseMessage) return super(MultiResponseMessage, cls).__new__(cls, *args, **kwargs) def __init__(self, diff --git a/morpheus/utils/logger.py b/morpheus/utils/logger.py index 693e6574b7..8fbe5aa2c2 100644 --- a/morpheus/utils/logger.py +++ b/morpheus/utils/logger.py @@ -19,6 +19,8 @@ import logging.handlers import multiprocessing import os +import re +import warnings from enum import Enum import appdirs @@ -26,6 +28,8 @@ import mrc from tqdm import tqdm +import morpheus + LogLevels = Enum('LogLevels', logging._nameToLevel) @@ -223,9 +227,14 @@ def deprecated_stage_warning(logger, cls, name, reason: str = None): logger.warning(message) -def deprecated_message_warning(logger, cls, new_cls): +def deprecated_message_warning(cls, new_cls): """Log a warning about a deprecated message.""" - logger.warning( - ("The '%s' message has been deprecated and will be removed in a future version. Please use '%s' instead."), - cls.__name__, - new_cls.__name__) + match = re.match(r"(\d+\.\d+)", morpheus.__version__) + if match is None: + version = "next version" + else: + version = "version " + match.group(1) + + message = (f"The '{cls.__name__}' message has been deprecated and will be removed " + f"after {version} release. Please use '{new_cls.__name__}' instead.") + warnings.warn(message, DeprecationWarning) diff --git a/tests/test_logger.py b/tests/test_logger.py index dddf05d345..6bcb8c3460 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -16,6 +16,7 @@ import logging import multiprocessing import os +import re from unittest.mock import patch import pytest @@ -142,7 +143,7 @@ class DummyStage(): "This is the reason." in caplog.text -def test_deprecated_message_warning(caplog): +def test_deprecated_message_warning(): class OldMessage(): pass @@ -150,10 +151,14 @@ class OldMessage(): class NewMessage(): pass - logger = logging.getLogger() - caplog.set_level(logging.WARNING) - deprecated_message_warning(logger, OldMessage, NewMessage) - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "WARNING" - assert "The 'OldMessage' message has been deprecated and will be removed in a future version. " \ - "Please use 'NewMessage' instead." in caplog.text + with pytest.warns(DeprecationWarning) as warnings: + deprecated_message_warning(OldMessage, NewMessage) + + pattern_with_version = (r"The '(\w+)' message has been deprecated and will be removed " + r"after version (\d+\.\d+) release. Please use '(\w+)' instead.") + + pattern_without_version = (r"The '(\w+)' message has been deprecated and will be removed " + r"after next version release. Please use '(\w+)' instead.") + + assert (re.search(pattern_with_version, str(warnings[0].message)) is not None) or\ + (re.search(pattern_without_version, str(warnings[0].message))) diff --git a/tests/test_multi_message.py b/tests/test_multi_message.py index 4c6fd5e70d..26e349e10d 100644 --- a/tests/test_multi_message.py +++ b/tests/test_multi_message.py @@ -19,6 +19,7 @@ import dataclasses import string import typing +from unittest.mock import patch import cupy as cp import numpy as np @@ -28,6 +29,7 @@ import cudf from _utils.dataset_manager import DatasetManager +from morpheus.messages import ControlMessage from morpheus.messages.memory.inference_memory import InferenceMemory from morpheus.messages.memory.response_memory import ResponseMemory from morpheus.messages.memory.response_memory import ResponseMemoryProbs @@ -42,6 +44,7 @@ from morpheus.messages.multi_response_message import MultiResponseMessage from morpheus.messages.multi_response_message import MultiResponseProbsMessage from morpheus.messages.multi_tensor_message import MultiTensorMessage +from morpheus.utils import logger as morpheus_logger @pytest.mark.use_python @@ -800,3 +803,42 @@ def test_tensor_slicing(dataset: DatasetManager): assert double_slice.count == single_slice.count assert cp.all(double_slice.get_tensor("probs") == single_slice.get_tensor("probs")) dataset.assert_df_equal(double_slice.get_meta(), single_slice.get_meta()) + + +@pytest.mark.usefixtures("use_cpp") +@pytest.mark.use_python +def test_deprecation_message(filter_probs_df: cudf.DataFrame, caplog): + + meta = MessageMeta(filter_probs_df) + + multi_tensor_message_tensors = { + "input_ids": cp.zeros((20, 2)), + "input_mask": cp.zeros((20, 2)), + "seq_ids": cp.expand_dims(cp.arange(0, 20, dtype=int), axis=1), + "input__0": cp.zeros((20, 2)), + "probs": cp.zeros((20, 2)), + } + + def generate_deprecation_warning(deprecated_class, new_class): + + # patching warning.warn here to get the warning message string from deprecated_message_warning() for asserting + with patch("warnings.warn") as mock_warning: + morpheus_logger.deprecated_message_warning(deprecated_class, new_class) + warning_msg = mock_warning.call_args.args[0] + + return warning_msg + + with pytest.warns(DeprecationWarning) as warnings: + MultiMessage(meta=meta) + MultiAEMessage(meta=meta, model=None) + MultiTensorMessage(meta=meta, memory=TensorMemory(count=20, tensors=multi_tensor_message_tensors)) + MultiResponseMessage(meta=meta, memory=TensorMemory(count=20, tensors=multi_tensor_message_tensors)) + MultiInferenceMessage(meta=meta, memory=TensorMemory(count=20, tensors=multi_tensor_message_tensors)) + MultiInferenceAEMessage(meta=meta, memory=TensorMemory(count=20, tensors=multi_tensor_message_tensors)) + + assert str(warnings[0].message) == generate_deprecation_warning(MultiMessage, ControlMessage) + assert str(warnings[1].message) == generate_deprecation_warning(MultiAEMessage, ControlMessage) + assert str(warnings[2].message) == generate_deprecation_warning(MultiTensorMessage, ControlMessage) + assert str(warnings[3].message) == generate_deprecation_warning(MultiResponseMessage, ControlMessage) + assert str(warnings[4].message) == generate_deprecation_warning(MultiInferenceMessage, ControlMessage) + assert str(warnings[5].message) == generate_deprecation_warning(MultiInferenceAEMessage, ControlMessage)