All Products
Search
Document Center

Data Lake Formation:Read DLF multimodal tables using PyPaimon

Last Updated:Sep 17, 2026

Learn how to read BLOBs from DLF multimodal tables with PyPaimon, covering both local and Ray-based distributed reads.

Prerequisites

Install the following dependencies before you begin:

pip install pyjindosdk
pip install pypaimon-1.5.dev20260727.tar.gz
Note
  • To get the PyPaimon package, click pypaimon-1.5.dev20260727.tar.gz.

  • We recommend installing pyjindosdk in production environments. Reading from OSS through pyjindosdk avoids falling back to the legacy OSS path, which maintains stable performance under high-concurrency reads.

Initialize the connection

Before reading a multimodal table, connect to the catalog and obtain a table object. All read scenarios below — local, Ray distributed, and Daft on Ray — reuse this table object.

import pypaimon.multimodal as pm

catalog_options = {
    "metastore": "rest",
    "uri": "<DLF_ENDPOINT>",
    "warehouse": "<CATALOG_NAME>",
    "token.provider": "dlf",
    "dlf.region": "<REGION_ID>",
    "dlf.oss-endpoint": "<OSS_ENDPOINT>",
    "dlf.access-key-id": "<ACCESS_KEY_ID>",
    "dlf.access-key-secret": "<ACCESS_KEY_SECRET>",
    "dlf.security-token": "<SECURITY_TOKEN>",
}

conn = pm.connect(database="<DATABASE_NAME>", options=catalog_options)
table = conn.get_table("<TABLE_NAME>")

Connection parameters

Parameter

Description

<DATABASE_NAME>

The name of the Paimon database.

<DLF_ENDPOINT>

The DLF endpoint, for example, http://<region-id>-vpc.dlf.aliyuncs.com. For more information, see DLF Endpoint.

<CATALOG_NAME>

The name of the DLF catalog.

<REGION_ID>

The region ID, for example cn-hangzhou.

<OSS_ENDPOINT>

The OSS endpoint, for example, oss-<region-id>-internal.aliyuncs.com. For more information, see OSS regions and endpoints.

<ACCESS_KEY_ID>

Your Alibaba Cloud AccessKey ID.

<ACCESS_KEY_SECRET>

Your Alibaba Cloud AccessKey Secret.

<SECURITY_TOKEN>

The security token from an STS temporary credential. Omit this parameter if you use a long-term AccessKey pair.

<TABLE_NAME>

The name of the multimodal table.

Local reads

Read a small dataset at once

To read one clip or a few clips that fit in memory, use read_blobs to load all data at once.

scalar, blobs = (
    table.scan()
    .where("clip_id = 'xxx'")
    .select(["clip_id", "frame_index", "camera_0", "camera_1", "camera_2", "camera_3"])
    .read_blobs(
        ["camera_0", "camera_1", "camera_2", "camera_3"],
        parallelism=16,
    )
)

Stream large volumes of data

To read many clips without loading all BLOB bytes into memory at once, use stream_blobs to consume data in batches.

clip_ids = ["clip_001", "clip_002", "clip_003"]
clip_filter = "clip_id IN (" + ", ".join(f"'{clip_id}'" for clip_id in clip_ids) + ")"

for scalar_batch, blobs in (
    table.scan()
    .where(clip_filter)
    .select(["clip_id", "frame_index", "camera_0", "camera_1", "camera_2", "camera_3"])
    .stream_blobs(
        ["camera_0", "camera_1", "camera_2", "camera_3"],
        parallelism=16,
    )
):
    consume_batch(scalar_batch, blobs)

Parameters

Parameter

Description

parallelism

Number of threads for reading BLOBs.

batch_size

Number of rows per batch in stream_blobs. Default: 1024. You can also set this through the table-level read.batch-size dynamic parameter.

Read consecutive frame ranges for training

Training workloads often require a consecutive frame window — for example, 16, 32, or 64 frames — from the same clip. The following example reads a consecutive frame range within a single clip.

start = 1000
read_frames = 512

for scalar_batch, blobs in (
    table.scan()
    .where(
        f"clip_id = 'xxx' "
        f"AND frame_index >= {start} "
        f"AND frame_index < {start + read_frames}"
    )
    .select(["clip_id", "frame_index", "camera_0", "camera_1", "camera_2", "camera_3"])
    .stream_blobs(
        ["camera_0", "camera_1", "camera_2", "camera_3"],
        parallelism=16,
    )
):
    consume_training_range(scalar_batch, blobs)

Distributed BLOB processing with Ray

For Ray-based workloads, use a two-step approach for distributed reads:

  1. Call scan().to_ray() to read scalar columns and BLOB descriptors across workers.

  2. Call table.map_with_blobs() to read BLOB bytes on each Ray worker and pass them to a user-defined function (UDF).

Basic usage

import ray
import pyarrow as pa

ray.init(address="auto", ignore_reinit_error=True)

clip_ids = ["clip_001", "clip_002", "clip_003"]
scalar_cols = ["clip_id", "frame_index"]
blob_cols = ["camera_0", "camera_1", "camera_2", "camera_3"]
select_cols = scalar_cols + blob_cols
clip_filter = "clip_id IN (" + ", ".join(f"'{clip_id}'" for clip_id in clip_ids) + ")"

def process_batch(scalar_batch, blobs):
    """
    scalar_batch: pyarrow.Table containing scalar columns such as clip_id and frame_index.
    blobs: dict[str, list[bytes | None]], keyed by blob column name.
    Must return a small pyarrow.Table. Return an empty table for side-effect-only processing.
    """
    camera_0 = blobs["camera_0"]
    # Avoid returning raw BLOB bytes to prevent materializing large payloads in Ray's object store.
    return pa.table({
        "rows": [scalar_batch.num_rows],
    })

ds = (
    table.scan()
    .where(clip_filter)
    .select(select_cols)
    .to_ray()
)

result_ds = table.map_with_blobs(
    ds,
    blob_cols,
    process_batch,
)

# Trigger execution. Ray Dataset is lazy — BLOBs are not read until result_ds is consumed.
for _ in result_ds.iter_batches(batch_format="pyarrow"):
    pass

Parameters

Parameter

Description

to_ray()

Ray automatically determines the read concurrency and block count based on available resources and data size. You typically do not need to configure this.

parallelism

Number of threads within each Ray task for reading BLOB bytes. This is not the number of Ray workers. Default: 64.

batch_size

Number of rows passed to the UDF per call. Default: 1024. If BLOBs are large or worker memory is limited, reduce this to 128, 256, or 512.

Configure Ray task retries

For large-scale cold-read scenarios, configure Ray task retries for map_with_blobs().

result_ds = table.map_with_blobs(
    ds,
    blob_cols,
    process_batch,
    parallelism=8,
    batch_size=512,
    ray_remote_args={
        "max_retries": 3,
        "retry_exceptions": True,
    },
)

If the descriptor volume is also large, pass the same ray_remote_args to to_ray().

Distributed BLOB processing with Daft on Ray

Daft on Ray provides an alternative distributed read path. It is a good fit when you already have a Daft-based pipeline.

Basic usage

import datetime as dt

import daft
from daft import col, runners
import pyarrow as pa
import ray

from pypaimon.daft import read_paimon, read_blob

ray.init(address="auto", ignore_reinit_error=True)
runners.set_runner_ray(address="auto", noop_if_initialized=True)

table_identifier = "<DATABASE_NAME>.<TABLE_NAME>"

clip_ids = ["clip_001", "clip_002", "clip_003"]
scalar_cols = ["clip_id", "frame_index", "collected_date"]
blob_cols = ["camera_0", "camera_1", "camera_2", "camera_3"]
target_date = dt.date(2026, 7, 1)

df = read_paimon(table_identifier, catalog_options)

df = df.where(
    (col("collected_date") == target_date)
    & col("clip_id").is_in(clip_ids)
)

# read_blob reads Daft File/Blob descriptor columns into binary bytes.
# max_concurrency controls read concurrency within each Daft batch UDF.
blob_bytes_cols = []
for blob_col in blob_cols:
    bytes_col = blob_col.replace(".", "_") + "_bytes"
    blob_bytes_cols.append(bytes_col)

df = df.select(
    *[col(name) for name in scalar_cols],
    *[
        read_blob(
            col(blob_col),
            catalog_options,
            table_identifier,
            max_concurrency=16,
        ).alias(bytes_col)
        for blob_col, bytes_col in zip(blob_cols, blob_bytes_cols)
    ],
)

# Example: aggregate byte sizes only. In production, chain decode, preprocess, inference, or write logic.
# Avoid retaining or writing raw BLOB bytes to prevent large payloads from entering Ray's object store.
agg_exprs = [col("clip_id").count().alias("rows")]
for bytes_col in blob_bytes_cols:
    agg_exprs.append(col(bytes_col).count().alias(bytes_col + "_count"))
    agg_exprs.append(col(bytes_col).str.length().sum().alias(bytes_col + "_sum"))

result = df.agg(*agg_exprs).collect()

Open individual blob streams with open_blob

To open and process BLOBs individually inside a custom Daft UDF, use open_blob(). The max_concurrency parameter controls read concurrency within each Daft batch UDF.

from pypaimon.daft import open_blob

def consume_one_blob(file):
    with open_blob(file, catalog_options, table_identifier) as stream:
        data = stream.read()
        # decode / preprocess / inference / write
        return len(data)