Hi Team I'm trying to replicate daft image benchm...
# daft-dev
a
Hi Team I'm trying to replicate daft image benchmark to ensure i can get similar throughput, but I'm not seeing it work for me I'm using
0.6.8
. The code works in 1 gpu but when I increase the number of GPUs is still take the same amount of time and I only see 1 GPU busy in my ray cluster. My current setup is I have 1 head node and 4 gpu-worker node. Each gpu worker is a kuberay pod that has 4 GPUs (using A10G GPUs). I tested with 1 GPU and getting 40ish minutes. I tested with 8 GPUs (2 nodes) and its still 40ish minutes. Seems like it is using only 1 GPU even when I tell it to use eight. Note: the node has 16 gpus total and 4 gpus nodes. I also see a swordfish actor created for the nodes even if I only select 8 gpus instead which should only need 2 nodes technically...
✅ 1
Code:
Copy code
from __future__ import annotations

import numpy as np
import torch
from torchvision import transforms
from torchvision.models import ResNet18_Weights, resnet18
import time
import ray

import daft
from daft import col


import logging
import os

import hydra
import ray
from omegaconf import DictConfig
from data_pipelines.common.logging import setup_logger


# # Wait for Ray cluster to be ready
# @ray.remote
# def warmup():
#     pass
# ray.get([warmup.remote() for _ in range(NUM_GPU_NODES * 23)])


weights = ResNet18_Weights.DEFAULT
transform = transforms.Compose([transforms.ToTensor(), weights.transforms()])


class ResNetModel:
    def __init__(self):
        self.weights = weights
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        print(f"Device: {self.device}")
        self.model = resnet18(weights=weights).to(self.device)
        self.model.eval()

    @daft.method.batch(
        return_dtype=daft.DataType.string(),
        batch_size=10,
    )
    def __call__(self, images):
        if len(images) == 0:
            return []
        torch_batch = torch.from_numpy(np.array(images.to_pylist())).to(self.device)
        with torch.inference_mode():
            prediction = self.model(torch_batch)
            predicted_classes = prediction.argmax(dim=1).detach().cpu()
            predicted_labels = [self.weights.meta["categories"][i] for i in predicted_classes]
            return predicted_labels


def daft_benchmark(cfg: DictConfig, logger: logging.Logger, **kwargs) -> None:
    """Build eye-gaze action dataset: query Databricks, join with S3 images/labels, write Parquet.

    Reads actions from Databricks, expands to per-frame rows, resolves image and label URLs
    via Daft UDFs, filters to labeled frames, and writes action and image Parquet tables.

    Args:
        cfg: Hydra config with pipeline, env_vars, databricks, ray, logging.
        logger: Logger for progress and debug.
        **kwargs: Additional options passed through.

    Examples:
        Called by Hydra from CLI:
            python data_pipelines/run_pipeline.py pipeline=dms/daft_benchmark
        Or from code with a DictConfig and logger:
            daft_benchmark(cfg, logger)
    """
    model = daft.cls(ResNetModel, gpus=1, max_concurrency=cfg.ray.num_gpus)
    start_time = time.time()
    <http://logger.info|logger.info>(f"Reading S3 Images from from {cfg.pipeline.input_path}")
    df = daft.from_glob_path(f"{cfg.pipeline.input_path}/**/*.png")
    df = df.with_column_renamed("path", "image_url")
    df = df.collect()
    <http://logger.info|logger.info>(f"Number of Images: {df.count_rows()}")
    df = df.with_column(
        "decoded_image",
        df["image_url"].download().decode_image(mode=daft.ImageMode.RGB),
    )
    df = df.with_column(
        "norm_image",
        df["decoded_image"].apply(
            func=lambda image: transform(image),
            return_dtype=daft.DataType.tensor(dtype=daft.DataType.float32(), shape=tuple(cfg.pipeline.image_dim)),
        ),
    )
    df = df.with_column("label", model()(col("norm_image")))
    df = df.select("image_url", "label")
    <http://logger.info|logger.info>(f"Schema: {df.schema()}")
    df.write_parquet(f"{cfg.pipeline.output_path}", write_mode="overwrite")
    end_time = time.time()
    <http://logger.info|logger.info>(f"Time taken: {end_time - start_time} seconds")
    <http://logger.info|logger.info>(f"Images per second: {df.count_rows() / (end_time - start_time)}")
    <http://logger.info|logger.info>(f"Parquet written to {cfg.pipeline.output_path}")
Omitted the hydra init but basically just calling daft_benckmark with a cfg and logger as input..
👀 1
@Cory Grinstead A bit more context. If I run the exact same daft_main.py using daft public data example I see one plan in each worker and the GPUs being utilized as expect on all workers. Since I have 4 nodes, 16 gpus but only requested 8 gpus for the job it used 2 gpus per node. But when I change to using
df = daft.from_glob_path(f"{cfg.pipeline.input_path}/**/*.png")
it seems to not use the GPUs I asked them to used. Seems to be the main core difference in my modified code... Should
daft.from_glob_path
be able to scale or because it's a glob_path it only maps to 1 gpu?
c
@Abner Ayala I think this could be directly related to https://github.com/Eventual-Inc/Daft/pull/5897. We are not parallelizing on from_glob_path, so its only creating a single task. therefore the max parallelism is always going to be 1.
a
thanks @Cory Grinstead. is there a way to force the parallelism after glob? by materilizing first (collect) and maybe doing df.partition?
c
Yes you could materialize then do
into_partitions
as a workaround, but the better longterm solution would be adding parallelism at the source like #5897 does.
❤️ 1
s
Just merged the PR for it, so it should be available in 0.7.6 next week
daft party 1
a
btw what is the difference between into_partitions and repartition? When to use one over the other?
s
generally its recommended to use
into_partitions
if you're just looking to resize or split partitions.
repartition
is useful if you're looking to reorganize the data based on some property, such as a hash, range, or completely randomly