Rechunker

repository·master·Indexed 41 days ago

https://github.com/pangeo-data/rechunker

A Python library for rechunking large-scale chunked arrays, such as Zarr and TileDB, optimized for parallel execution via Dask. It uses a Push/Pull Consolidated algorithm to transition data from a source to a target chunking scheme while respecting memory limits (max_mem) and minimizing tasks. The library supports rechunking single arrays, Zarr groups, and Xarray datasets, with multiple executor backends including Dask, Beam, Prefect, Pywren, and Python.

Tokens
9.6K
Snippets
30
Records
38
Agent score
51%

What's inside Rechunker

  1. Overview of Rechunker

    master
    Rechunker is a Python package designed for efficient and scalable manipulation of chunk structures in chunked array formats like Zarr and TileDB. It allows you to take an input array (or a group of arrays) from a persistent storage device (e.g., a local filesystem or cloud storage bucket) and write it to a new location with a different chunking scheme. Rechunker is built to operate within parallel execution frameworks, specifically Dask, to handle large-scale data processing.
  2. Execute a rechunking plan with the Rechunked object

    master

    Calling rechunker.rechunk returns a Rechunked object. This object represents a plan, but it does not perform any work immediately. To start the rechunking process, you must explicitly call the .execute() method on the Rechunked object.

    Important Lifecycle Management:

    • Execution: The operation only begins when .execute() is called.
    • Cleanup: You are responsible for manually deleting the intermediate store once the .execute() method has finished to free up storage space.
    # Conceptual workflow
    rechunked_obj = rechunker.rechunk(...)
    rechunked_obj.execute()
    # Manually delete intermediate store here
  3. How the Push / Pull Consolidated algorithm works

    master

    Rechunker uses the Push / Pull Consolidated algorithm to rechunk datasets while satisfying three primary constraints: respecting memory limits, minimizing the number of required tasks (always less than N source chunks + M target chunks), and ensuring the process is embarrassingly parallel (avoiding write locks and inter-worker communication).

    The algorithm follows these steps:

    1. Input Configuration: The user provides a source array (shape, chunk structure, and data type), the desired target_chunks structure, and a max_mem threshold.
    2. Determine Write Batches: The algorithm calculates the largest batch of data a single worker can write without exceeding max_mem. These are called write_chunks.
    3. Determine Read Batches: The algorithm calculates the largest batch of data a single worker can read given max_mem, attempting to align these with write_chunks where possible. These are called read_chunks.
    4. Direct Copy Path: If write_chunks == read_chunks, the algorithm performs a direct copy from the source to the target, avoiding intermediate datasets.
    5. Intermediate Path: If they do not match, the algorithm defines intermediate chunks as the minimum of write_chunks and read_chunks along each axis. Data is first copied from the source to an intermediate array, and then from the intermediate array to the target.
  4. Execute rechunker plans with different executors

    master

    Rechunker plans can be executed using various executors. The default executor is dask, which runs the plan on a Dask cluster. For debugging purposes or simple local tasks, you can use the python executor, which executes the plan as a standard Python for loop.

    Note that most executors require installing additional optional dependencies beyond the core rechunker package.

    import zarr
    from rechunker import rechunk
    
    source = zarr.ones((4, 4), chunks=(2, 2), store="source.zarr")
    intermediate = "intermediate.zarr"
    target = "target.zarr"
    
    # Using the 'python' executor for simple execution
    rechunked = rechunk(
        source, 
        target_chunks=(4, 1), 
        target_store=target,
        max_mem=256000000, 
        temp_store=intermediate,
        executor="python"
    )
    
    rechunked.execute()
  5. How the `Rechunked` object works

    master

    A Rechunked object is a handle for a deferred rechunking operation. It encapsulates the execution engine (executor), the scheduling plan, and the paths to the source, intermediate, and target data.

    When you call rechunk(), no data is moved immediately; instead, a Rechunked object is returned. The actual computation is triggered only when .execute() is called. This allows you to inspect the plan or pass the handle around before committing to the heavy computation.

    source = zarr.ones((4, 4), chunks=(2, 2), store="source.zarr")
    rechunked = rechunk(source, target_chunks=(4, 1), target_store="target.zarr", max_mem=256000, temp_store="temp.zarr")
    
    # The plan can be inspected (type depends on executor)
    print(rechunked.plan)
    
    # The actual work happens here
    final_array = rechunked.execute()
  6. How intermediate chunks are calculated

    master

    The rechunking algorithm uses a 'Greatest Common Denominator' (GCD) approach to find intermediate chunks.

    1. GCD Chunks: It first identifies the smallest possible chunks that evenly fit into both the source and target chunks using the GCD of the dimensions. These are guaranteed to be smaller than both source and target chunks.
    2. Aggregation: To improve performance, the algorithm attempts to increase these GCD chunks by aggregating them along axes. It checks how many source chunks can be combined before exceeding the max_mem limit.
    3. Optimization: It iterates through possible aggregation axes (starting from the highest axis) to maximize the chunk size while respecting the memory constraint and the target chunk requirements.
  7. Construct execution workflows with Stage and Pipeline

    master

    Rechunker uses a staged approach to define execution workflows.

    Stage

    A Stage is a frozen dataclass representing a single step in a workflow. It contains:

    • function: A StageFunction, which can be a function taking no arguments (except config) or a function taking a single config argument.
    • name: A string identifier for the stage.
    • mappable: An optional Iterable used for mapping operations.

    Pipeline

    A Pipeline is a frozen dataclass that groups a sequence of Stage objects together with an optional config object.

    ParallelPipelines

    A ParallelPipelines type is a Tuple of one or more Pipeline objects intended to be executed in parallel.

    from rechunker.types import Stage, Pipeline
    
    def my_stage_func(config=None):
        pass
    
    stage = Stage(function=my_stage_func, name="my_stage")
    pipeline = Pipeline(stages=[stage], config={"param": 1})
    parallel_pipelines = (pipeline,)
  8. Rechunk a single Zarr array

    master

    To rechunk a single array, use the rechunk function. You must provide the source array, the desired target_chunks (as a tuple or a dictionary mapping dimension names to sizes), the max_mem allowed for intermediate steps, a target_store for the final data, and a temp_store for intermediate data.

    Note that rechunk returns a Rechunked object, which represents a plan. The actual rechunking process is only performed when you call the .execute() method on this object. Rechunking can be performed using the default Dask multi-threaded scheduler or by providing a Dask Client (e.g., from a LocalCluster or GatewayCluster) to use a distributed cluster.

    from rechunker import rechunk
    
    target_chunks = {"time": 2920, "lat": 25, "lon": 1}
    max_mem = "1MB"
    target_store = "air_rechunked.zarr"
    temp_store = "air_rechunked-tmp.zarr"
    
    # Create the plan
    array_plan = rechunk(
        source_array, 
        target_chunks, 
        max_mem, 
        target_store, 
        temp_store=temp_store
    )
    
    # Execute the plan
    result = array_plan.execute()
  9. Rechunk data in cloud storage (GCS)

    master

    Rechunker can be used to rechunk data stored in cloud buckets (like Google Cloud Storage) by using a filesystem mapper (e.g., gcsfs).

    1. Use gcsfs.GCSFileSystem to create a filesystem object.
    2. Use gcs.get_mapper(url) to create a mapper for the source and target locations.
    3. Pass these mappers to the rechunk function as the target_store and temp_store.
    4. For large-scale cloud rechunking, it is recommended to use a distributed Dask cluster (e.g., via Dask Gateway).
    import gcsfs
    from rechunker import rechunk
    from dask.distributed import Client
    
    # Setup cluster and filesystem
    client = Client() # or a GatewayCluster
    gcs = gcsfs.GCSFileSystem(requester_pays=True)
    
    # Define locations using mappers
    source_store = gcs.get_mapper("gs://pangeo-cmems-duacs")
    target_store = gcs.get_mapper("gs://your-bucket/target.zarr")
    temp_store = gcs.get_mapper("gs://your-bucket/temp.zarr")
    
    # Open source array from group
    import zarr
    group = zarr.open_consolidated(source_store, mode="r")
    source_array = group["sla"]
    
    # Plan and execute
    plan = rechunk(source_array, (8901, 72, 72), "1GB", target_store, temp_store=temp_store)
    plan.execute()
  10. Rechunk a Zarr Group or Xarray Dataset

    master

    Rechunker supports rechunking entire groups (to preserve metadata/coordinates) or Xarray Datasets.

    When rechunking a group, target_chunks must be a dictionary where keys are variable names. For each variable, you can specify:

    • A dictionary of dimension-to-chunksize mappings (e.g., {"time": 2920, "lat": 25, "lon": 1}).
    • None to indicate that the specific array should not be rechunked.

    If you provide a simple dictionary mapping dimension names to chunk sizes (e.g., {"time": 2920, "lat": 25, "lon": 1}) directly to the rechunk function when passing an Xarray Dataset, rechunker will apply those consistent chunks to all variables in the dataset.

    # Rechunking a group with specific settings per variable
    target_chunks = {
        "air": {"time": 2920, "lat": 25, "lon": 1},
        "time": None,  # don't rechunk this array
        "lon": None,
        "lat": None,
    }
    
    array_plan = rechunk(
        source_group, 
        target_chunks, 
        max_mem, 
        target_store, 
        temp_store=temp_store
    )
    array_plan.execute()
  11. Configure Zarr options for target and temporary stores

    master

    When rechunking, you can pass additional Zarr configuration via target_options and temp_options.

    Supported Zarr options include:

    • compressor
    • filters
    • order
    • cache_metadata
    • cache_attrs
    • overwrite
    • write_empty_chunks

    If the source is an xarray.Dataset, target_options are used to encode variables similarly to how the encoding parameter works in Dataset.to_zarr().