Skip to content

[Discussion] Serialize objects within tasks #4673

Open

Description

@gjoseph92

When Blockwise materializes its graph on the scheduler, we'd sometimes like to be able to insert bits of data from the client that aren't msgpack-serializable into that graph. Ideally, we could wrap these things in to_serialize on the client, access them as opaque Serialized blobs on the scheduler to insert them into tasks, and when the worker receives the task, the Serialized blobs within would automatically be deserialized.

However, most tasks are pre-serialized by dumps_task, which calls pickle directly. So if a task contains a Serialize object, the normal serialization machinery on the comm will never know, since it's just bytes by the time it reaches dumps. Same thing in the other direction. The reason for this special case is to cache serialized functions—particularly for numba functions, where we want to reuse the same function object instead of re-jitting every time.

I think our requirements are:

  1. If tasks contain Serialize or Serialized objects, those objects are automatically deserialized on the worker, but not on the scheduler.
  2. Serialized functions are cached, like they currently are with loads_function and dumps_function. That is, deserialize(serialize(func)) is deserialize(serialize(func)) (unless serialize(func) is huge). Okay if this doesn't apply to serialize in general and is specialized to tasks.
  3. Users still see the warn_dumps warning when submitting large tasks.

Nice-to-haves:

  1. __dask_distributed_*pack__ doesn't use dumps_msgpack/loads_msgpack directly; it just produces/consumes the marshalled objects (which may contain Serialize/Serialized) and lets comm serialization handle the rest. I think this separation of concerns would be nice for longer-term maintenance.
  2. make_blockwise_graph doesn't need a deserializing argument (doesn't call dumps_function/warn_dumps, or handle stringify_collection_keys / formatting tasks into a dict)—again, would be nice to separate concerns. I'm wondering if make_blockwise_graph could switch to a generator: dict(make_blockwise_graph(...)) would give you a plain dask, but Scheduler.update_graph_hlg would provide the logic to handle the key-stringification, reformatting to a task dict, and possibly even doing something to mark the function as needing to be cached. A generator could help separate concerns without adding the overhead of a full dict traversal.

Ideas:
I think the general idea is to not have dumps_task serialize to bytes anymore, but just return Serialize objects? Maybe even get rid of dumps_task entirely? This would delegate all serialization-to-bytes to the comm level.

  1. Move function-caching logic into core serialize/deserialize. For deserialize, this will lead to a lot of cache misses. Bad idea.
  2. Register a dask_serialize method for FunctionType that caches, then just calls pickle. Of course, this won't cache other callables besides functions. Mediocre idea.
  3. Register a dummy serialization family for "task", which caches, then just calls back into deserialize. By marking things as "task" when serializing, we can reduce cache misses. Kinda hackey, could work?

Alternatively, the quickest solution would be to leave everything as it is, and have worker._deserialize just call deserialize on the args/kwargs, to unpack any Serialize/Serialized objects they may contain. But I imagine that is undesirable, especially when we already have this nice machinery for handling nested serialization with to_serialize and Serialized.

Pitfalls:

  1. If tasks are being deserialized at the comm level, what will errors look like to users when unpickling a task fails (such as due to a missing import)? What will happen to the task from the scheduler's perspective?

cc @mrocklin @jrbourbeau @ian-r-rose @jakirkham @madsbk @rjzamora @quasiben: how do these requirements sound? What should be added or removed?

Activity

  1. rjzamora commented on Apr 5, 2021

    @rjzamora
    Member

    Thanks for writing this up @gjoseph92! This seems like a great summary. Just adding some notes in case they are helpful:

    However, most tasks are pre-serialized by dumps_task, which calls pickle directly. … The reason for this special case is to cache serialized functions—particularly for numba functions, where we want to reuse the same function object instead of re-jitting every time.

    Yes - The problem is that dumps_task usually calls warn_dumps on the task arguments (which just pickles the arguments together - hiding internal Serialized objects at deserialization time). When a Blockwise layer is materialized on the scheduler, we actually don’t use dumps_task at all, since the function was already serialized by the client. Instead we manually define a dict for each task in (almost) the same way dumps_task would have done: {“function": <already-serialized-function>, "args": warn_dumps(args)}.

    The current Blockwise approach is a problem because: (1) warn_dumps does not properly handle existing Serialized objects, (2) The manual-dict approach precludes serialization of in-lined tasks.

    If we ignore the nested-task case for now, a possible solution is to:



    • A) Add some simple logic to dumps_function to avoid re-pickling bytes objects (already-serialized functions). Since this utility is specifically designed to serialize functions, it should be fine to avoid serializing a bytes object. (This allows us to go back to using dumps_task when we construct a graph)
    • B) We add a warn_serialize function that does something similar to warn_dumps, but generates a Serialized object instead of pickled bytes.
    • C) In dumps_task, we add an option to use warn_serialize (instead of warn_dumps) to serialize the “args”/“kwargs” elements of the task dictionary. We can either do this when Serialized elements are detected in the task args, or we can add an optional argument.
    • D) We ensure that calling serialize will iterate through each task element (and create separate sub-header/frames) when warn_serialize is used. For example, this will always be done if something like [WIP] Distinguish tuples & lists in MsgPack serialization #4575 gets in, and can be controlled by an iterate_collection argument if Add iterate_collection argument to serialize #4641 is integrated.
    • E) Add very simple logic in _deserialize to avoid calling pickle.loads if the “args”/“kwargs” are not bytes objects (these elements will already be deserialized if warn_serialize was used on the scheduler).

    I can confirm that the steps above address the current function-caching and Serialized-object problems. However, to deal with nested (in-lined) tasks, we would need to do something ugly, like wrap the serialized function in a callable object when the graph is constructed on the scheduler. Therefore, in the long run, it may be cleanest use something similar to your last two ideas.

  2. added a commit that references this issue on Apr 6, 2021
  3. madsbk commented on Apr 8, 2021

    @madsbk
    Contributor

    Thanks @gjoseph92, I think you proposal is a reasonable approach but it got me thinking: maybe it is time to standardize tasks.

    What if we decouple the arguments of a task from the callable/function? Something like:

    1. Normalize the tasks such that task arguments are either constants (objects not containing any keys) or pure keys, nothing in-between! This normalization will be done automatically and might include wrapping the task in a SubgraphCallable in order to handle nested keys and/or tasks.

    2. Serialize the callable and its constant arguments of the normalized tasks. The pure key arguments doesn't need any serialization.

    3. Wrap the tasks in a class that clearly define what are the serialized callable, what are the serialized constant arguments, and what are the pure input keys.

    Now, the scheduler can calculate task dependencies, task annotations, stringify input keys, and modify input keys without having to deserialize anything.

  4. rjzamora commented on Apr 8, 2021

    @rjzamora
    Member

    maybe it is time to standardize tasks.

    Thanks for your thoughts here @madsbk ! I do get the impression that this is all further motivation for something like this. I have experimented with multiple ways to address the problem (both within serialize and with a new serialize_task utility) and they both feel like workarounds. I am trying to work out a serialize-based approach with minimal changes, but I think the long-term solution may require a new task specification.

  5. madsbk commented on Apr 22, 2021

    @madsbk
    Contributor

    @gjoseph92, FYI I am working on a PR streamlining serialization: #4699

  6. fjetter commented on Aug 4, 2023

    @fjetter
    Member

    FYI I have a WIP PR open that is removing dumps_task #8067 Now that we're much more generously pickling things I don't think this complexity is still warranted

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions